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

# Architecture

> Component layers, the engine facade pattern, the access-token hook and the key architectural decisions behind the B Brands IdP.

The IdP is **not** a from-scratch authorization server. It is a thin B Brands
facade over a **managed OAuth 2.1 / OIDC engine**, plus the identity, enablement,
audit and lifecycle layer that the engine does not provide.

## 1. Component layers

The system is composed of five logical layers living in distinct physical
pieces.

```mermaid theme={null}
flowchart TB
    L1["<b>Layer 1 — OAuth clients</b><br/>ERP, CRM, future platforms<br/>(they authorize their own users)"]
    L2["<b>Layer 2 — Identity origin + API gateway</b> · auth.bbrands.io → api.bbrands.io<br/>OAuth proxy /api/v3/oauth/* · OIDC discovery · token exchange · Resource Server (HS256) · webhook dispatcher"]
    L3["<b>Layer 3 — Identity</b><br/>Identity admin console<br/>OAuth client registry<br/>user × platform access<br/>audit"]
    L4["<b>Layer 4 — IdP engine</b><br/>Managed OIDC engine<br/>/oauth/authorize · /oauth/token<br/>JWKS · access-token hook"]
    L5["<b>Layer 5 — Downstream</b><br/>Each client platform<br/>manages its own permissions"]

    L1 -->|HTTPS / OAuth 2.1 / OIDC| L2
    L2 --> L3
    L2 --> L4
    L2 --> L5
    L4 --> L3
```

| Layer         | Components                        | Responsibility                                                                            |
| ------------- | --------------------------------- | ----------------------------------------------------------------------------------------- |
| 1. Clients    | ERP, CRM, etc.                    | Speak OIDC, store `client_id`/`secret`, render UI, **authorize their own users**          |
| 2. Gateway    | API gateway                       | The only public surface: proxy, consent redirect, exchange, resource validation, webhooks |
| 3. Identity   | Identity entities + admin console | Client catalogue, user × platform enablement, audit                                       |
| 4. IdP engine | Managed OIDC engine               | OAuth/OIDC protocol engine, identity token issuance, JWKS                                 |
| 5. Downstream | Integrated platforms              | Receive provisioning, keep identity mapping, define their own authorization               |

<Note>
  **What the IdP does NOT do**: it does not define application permissions,
  compute effective permissions, or inject roles/permissions into tokens.
  Fine-grained authorization lives in each client; the B Brands APIs authorize
  with their existing internal RBAC.
</Note>

## 2. How the code is organized

The IdP logic is a cross-cutting module, separate from the standard per-resource
pattern of the rest of the API.

| Surface                | Role                                                                     |
| ---------------------- | ------------------------------------------------------------------------ |
| `/api/v3/oauth/*`      | Public OAuth / OIDC endpoints                                            |
| `/.well-known/*`       | Discovery + JWKS                                                         |
| IdP module             | Repository, exchange, blacklist, management, webhooks, JWKS verification |
| Internal token signing | Internal HS256 tokens for the exchange                                   |
| Internal authorizer    | Internal RBAC + revocation (`jti`) blacklist check                       |
| Access-token hook      | Rewrites OIDC claims and enforces enablement                             |
| `/api/v3/idp/*`        | Admin / governance (clients, access, audit, provisioning)                |
| Identity schema        | Data model for the identity entities                                     |

## 3. Key architectural decisions (ADR-lite)

<AccordionGroup>
  <Accordion title="ADR-01 — A managed OAuth 2.1 engine as the protocol engine">
    We use a managed OAuth 2.1 engine as the authorization server: it is already
    our identity base, it implements the full protocol (PKCE, OIDC, JWKS, refresh
    rotation), and custom access-token hooks let us rewrite `iss`/`aud` and
    enforce enablement. Trade-off: we depend on the engine's capabilities; the
    lack of native custom scopes is irrelevant because we do not inject
    permissions into tokens.
  </Accordion>

  <Accordion title="ADR-02 — Full wrap of OAuth endpoints behind the B Brands origin">
    Every OAuth, OIDC, discovery and JWKS endpoint is served under a B Brands
    origin. Clients never see the engine's URLs, not even for discovery or JWKS.
    This allows swapping the identity backend without touching client config,
    and enables custom logic (rate limiting, audit, transformation) at a single
    origin.
  </Accordion>

  <Accordion title="ADR-03 (revised) — Canonical issuer = https://auth.bbrands.io (per environment)">
    The `iss` claim of issued access tokens is the environment's `auth.*` URL
    (e.g. `https://development.auth.bbrands.io` in development), rewritten by
    the access-token hook. A non-production token is never valid in production
    because clients validate `iss` against their own environment. This revises
    the original decision that named `api.*` as the issuer.
  </Accordion>

  <Accordion title="ADR-03b (revised) — A single user-facing identity origin: auth.*">
    The `auth.*` origin serves **both** the interactive surfaces (login,
    consent) and every protocol endpoint (discovery, JWKS, authorize, token,
    userinfo, revoke) — the latter through transparent rewrites to the `api.*`
    backend. This is the `accounts.google.com` shape: one stable identity
    origin for clients, while the implementation host behind it stays
    replaceable. Boot-time consistency checks and a JWKS availability monitor
    guard the contract.
  </Accordion>

  <Accordion title="ADR-04 — Asymmetric signing (ES256) for OAuth tokens">
    OAuth JWTs are signed with ES256 (ECC P-256); the public key is exposed via
    JWKS so clients can validate without sharing secrets. The internal B Brands
    JWT keeps using HS256 with a server-side secret. The two formats coexist and
    never mix.
  </Accordion>

  <Accordion title="ADR-04b — Per-client RS256 id_token re-signing">
    Clients whose OIDC library only validates RS256 (e.g. Odoo 18 with the OCA
    `auth_oidc` module) register `id_token_alg: "RS256"` (the OIDC
    `id_token_signed_response_alg`). For those clients only, the IdP verifies
    the genuine ES256 `id_token` and re-signs it with its own RSA key,
    preserving every claim. The JWKS publishes the RSA public key alongside the
    EC key, discovery advertises both algorithms, and the **access token is
    never re-signed**. Any re-signing failure falls back to the ES256 original.
  </Accordion>

  <Accordion title="ADR-05 — The IdP does not inject permissions into tokens">
    OAuth tokens carry only identity claims (`sub`, `email`, profile). There is no
    permission or role claim. The access-token hook only rewrites `iss`/`aud` and
    checks that the user is enabled for the client.
  </Accordion>

  <Accordion title="ADR-06 — A single source of truth for identity">
    The client catalogue, user × platform enablement and audit live in B Brands
    governance tables managed from the identity admin console. The engine only
    keeps what it needs to mint tokens.
  </Accordion>

  <Accordion title="ADR-07 — Pre-sync provisioning via webhook (not JIT)">
    Enabling access fires a webhook that creates/updates the user in the target
    platform before their first login. The payload carries identity and status,
    never roles. Deprovisioning is symmetric on suspend/revoke.
  </Accordion>

  <Accordion title="ADR-08 — Short TTL + blacklist for effective revocation">
    Short-lived OAuth access tokens + a short-lived exchange JWT + a blacklist
    checked on every validation. The exchange JWT must never inherit the long
    TTL of the traditional login.
  </Accordion>

  <Accordion title="ADR-09 — OIDC → internal JWT exchange to consume B Brands APIs">
    A client that needs to call the B Brands APIs on behalf of the user exchanges
    its OIDC token for an internal B Brands JWT and uses it as a Bearer. The
    existing internal authorizer validates it. This reuses the internal RBAC and
    the Resource Server with no changes. See
    [Token exchange](/oauth/token-exchange).
  </Accordion>
</AccordionGroup>

## 4. The central piece: the access-token hook

The hook does **not** inject permissions. Its job is to rewrite `iss`/`aud`,
validate that the user is enabled for the client and attach the
business-account dimension (`https://bbrands.io/profile`). If the user is not
enabled, no token is issued (failsafe deny).

```typescript theme={null}
// access-token hook (reference pseudocode)
async function accessTokenHook(request) {
  const { claims, user_id } = request;
  const clientId = claims.client_id; // from the OAuth engine

  // Non-OAuth session token: leave untouched.
  if (!clientId) return { claims };

  // OAuth token: resolve identity + enablement + account (no permissions).
  const result = await resolveOauthIdentity({ userId: user_id, clientId });

  if (!result?.enabled) {
    return { error: { http_code: 403, message: "access_denied_not_enabled" } };
  }

  return {
    claims: {
      ...claims,
      iss: ISSUER,                       // canonical per-environment auth.* issuer
      aud: AUDIENCE,                     // fixed IdP audience: "bbrands-idp"
      jti: claims.jti ?? generateJti(),  // guaranteed, drives the blacklist
      "https://bbrands.io/client_handle": result.client_handle,
      "https://bbrands.io/platform_type": result.platform_type,
      "https://bbrands.io/profile": {
        account: result.account,             // account bound via primary_user role
        odoo: result.account_external_id,    // account.external_id, null if unmapped
        user: claims.sub,
      },
    },
  };
}
```

The identity-resolution logic is described in [Data model](/oauth/data-model).
Failure mode: if the hook fails, **no token is issued** — security wins.

## 5. Per-client isolation

Although every client shares one engine instance, isolation is guaranteed by:

1. The **`https://bbrands.io/client_handle` claim** identifying the requesting client in each OAuth access token (`aud` is the fixed `bbrands-idp` audience).
2. **`client_handle` validation** by each client (a token for `crm-prod` is not valid in an ERP client).
3. **Enablement** via the user × platform access record, checked on issuance, exchange and refresh.
4. The **audit log** filtered by `client_handle`.

## 6. Deployment model

| Component                                 | Where it runs               | Notes                              |
| ----------------------------------------- | --------------------------- | ---------------------------------- |
| API gateway (proxy + exchange + resource) | The API hosting platform    | Same deployment as the current API |
| OAuth engine                              | Managed cloud service       | Production tier                    |
| Access-token hook                         | Edge function               | Same region as the engine          |
| Webhook dispatcher                        | Worker / queue              | Retry + dead-letter                |
| Blacklist                                 | Low-latency key-value store | Read-heavy, latency critical       |
| Identity admin console                    | Same app, admin area        | Protected by admin role            |
