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

# Token Exchange

> Convert an OIDC identity token into an internal B Brands JWT so a client can call the B Brands APIs on behalf of the user.

The OAuth ES256 token proves identity, but it is **never** accepted by the
B Brands business APIs. When a client needs to read/write B Brands data **on
behalf of the user**, it exchanges its OIDC token for an **internal B Brands
JWT** (the same HS256 format as the traditional login) and uses it as a Bearer.
The existing internal authorizer validates it with the user's own RBAC.

<Warning>
  **Two different "exchange" endpoints — do not confuse them:**

  * `POST /api/v3/oauth/exchange` → **external OAuth client** turns an OIDC token
    into an internal B Brands JWT (this page).
  * `POST /api/v3/auth/sso/exchange` → **internal cross-app** session handshake
    between B Brands apps. See [Social login](/oauth/social-login).
</Warning>

## POST `/api/v3/oauth/exchange`

Implements a simplified RFC 8693-style token exchange.

<ParamField body="subject_token" type="string" required>
  The OIDC token issued by the IdP for the user (id\_token or access\_token).
</ParamField>

<ParamField body="grant_type" type="string">
  Accepted for RFC 8693 parity; not enforced.
</ParamField>

<ParamField body="subject_token_type" type="string">
  Accepted for RFC 8693 parity; not enforced.
</ParamField>

### What the endpoint does

<Steps>
  <Step title="Verify the subject token">
    ES256 signature against JWKS, plus `iss`, `aud`, `exp`. Rejects expired or
    invalid tokens with `401`.
  </Step>

  <Step title="Check the blacklist">
    If the token `jti` is blacklisted, reject with `401`.
  </Step>

  <Step title="Resolve user and client">
    `user_id` from `sub`, client from the `https://bbrands.io/client_handle`
    claim. A missing claim returns `401`.
  </Step>

  <Step title="Validate enablement">
    The user × platform access must be active and the client must be active.
    Otherwise `403`.
  </Step>

  <Step title="Resolve the primary account & mint the JWT">
    Mint the internal HS256 JWT with `user_id`, `account_id`,
    `token_use: "exchange"`, the client handle and a `jti`, with a short TTL
    (default **900s**, configurable). Log the issuance with
    `grant_type = token_exchange`.
  </Step>
</Steps>

<CodeGroup>
  ```json Request theme={null}
  {
    "subject_token": "<OIDC id_token / access_token>",
    "subject_token_type": "id_token"
  }
  ```

  ```json Response 200 theme={null}
  {
    "message": "Token exchanged",
    "data": {
      "token": "<internal HS256 B Brands JWT>",
      "token_type": "Bearer",
      "expires_in": 900,
      "account": "<account_id or null>"
    },
    "meta": { "correlation": "...", "status": 200, "timestamp": "..." }
  }
  ```
</CodeGroup>

### Errors

| HTTP | Condition                                                                       |
| ---- | ------------------------------------------------------------------------------- |
| 400  | `subject_token` missing/empty                                                   |
| 401  | Invalid/expired OIDC token, blacklisted `jti`, or missing `client_handle` claim |
| 403  | Inactive/unknown client, or user not enabled for the platform                   |

## Using the exchanged token

The client uses the returned `token` as `Authorization: Bearer ...` against the
B Brands APIs. The existing internal authorizer validates the HS256 JWT and
resolves the **user's own** permissions (internal RBAC). The Resource Server does
not change.

```python theme={null}
import requests

# 1) Exchange the OIDC token for an internal B Brands JWT
r = requests.post(
    "https://api.bbrands.io/api/v3/oauth/exchange",
    json={"subject_token": id_token, "subject_token_type": "id_token"},
    timeout=10,
)
r.raise_for_status()
bbrands_token = r.json()["data"]["token"]

# 2) Call the B Brands API on behalf of the user
resp = requests.get(
    "https://api.bbrands.io/api/v3/account/account",
    headers={"Authorization": f"Bearer {bbrands_token}"},
    timeout=30,
)
```

## Security notes

<Warning>
  The exchanged JWT carries the user's **full** permissions in B Brands (no
  per-client least-privilege). This is acceptable for trusted first-party
  clients. If scoping is required, introduce it in the exchange (limit the
  permission set per client).
</Warning>

* **Short TTL is mandatory** (default 900s). Never emit the long-lived
  traditional login token through this flow.
* The token is **revocable** via the blacklist by `jti`. Both this endpoint and
  the internal authorizer reject blacklisted tokens.
* There is **no refresh** for the internal token by design. When it expires, the
  client re-runs the exchange with a still-valid OIDC token.

## Machine-to-machine (no user)

For processes without an interactive user (e.g. a sync cron), do **not** use OAuth
or this exchange. Use the existing B Brands **API key** model, provisioned with
least privilege. See [Client integration](/oauth/client-integration).
