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

# Intercambio de tokens

> Convierte un token de identidad OIDC en un JWT interno de B Brands para que un cliente pueda llamar a las APIs de B Brands en nombre del usuario.

El token OAuth ES256 demuestra la identidad, pero **nunca** es aceptado por las
APIs de negocio de B Brands. Cuando un cliente necesita leer/escribir datos de B Brands **en
nombre del usuario**, intercambia su token OIDC por un **JWT interno de B Brands**
(el mismo formato HS256 que el login tradicional) y lo usa como Bearer.
El autorizador interno existente lo valida con el propio RBAC del usuario.

<Warning>
  **Dos endpoints de "exchange" diferentes — no los confundas:**

  * `POST /api/v3/oauth/exchange` → un **cliente OAuth externo** convierte un token OIDC
    en un JWT interno de B Brands (esta página).
  * `POST /api/v3/auth/sso/exchange` → handshake de sesión **entre apps internas**
    de B Brands. Consulta [Inicio de sesión social](/es/oauth/social-login).
</Warning>

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

Implementa un intercambio de tokens simplificado al estilo RFC 8693.

<ParamField body="subject_token" type="string" required>
  El token OIDC emitido por el IdP para el usuario (id\_token o access\_token).
</ParamField>

<ParamField body="grant_type" type="string">
  Aceptado por paridad con RFC 8693; no se exige.
</ParamField>

<ParamField body="subject_token_type" type="string">
  Aceptado por paridad con RFC 8693; no se exige.
</ParamField>

### Qué hace el endpoint

<Steps>
  <Step title="Verifica el subject token">
    Firma ES256 contra JWKS, más `iss`, `aud`, `exp`. Rechaza tokens expirados o
    inválidos con `401`.
  </Step>

  <Step title="Verifica la blacklist">
    Si el `jti` del token está en la blacklist, rechaza con `401`.
  </Step>

  <Step title="Resuelve usuario y cliente">
    `user_id` desde `sub`, el cliente desde el claim `https://bbrands.io/client_handle`.
    Un claim faltante devuelve `401`.
  </Step>

  <Step title="Valida la habilitación">
    El acceso usuario × plataforma debe estar activo y el cliente debe estar activo.
    De lo contrario `403`.
  </Step>

  <Step title="Resuelve la cuenta primaria y acuña el JWT">
    Acuña el JWT interno HS256 con `user_id`, `account_id`,
    `token_use: "exchange"`, el handle del cliente y un `jti`, con un TTL corto
    (por defecto **900s**, configurable). Registra la emisión con
    `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>

### Errores

| HTTP | Condición                                                                          |
| ---- | ---------------------------------------------------------------------------------- |
| 400  | `subject_token` faltante/vacío                                                     |
| 401  | Token OIDC inválido/expirado, `jti` en blacklist, o claim `client_handle` faltante |
| 403  | Cliente inactivo/desconocido, o usuario no habilitado para la plataforma           |

## Uso del token intercambiado

El cliente usa el `token` devuelto como `Authorization: Bearer ...` contra las
APIs de B Brands. El autorizador interno existente valida el JWT HS256 y
resuelve los permisos **propios del usuario** (RBAC interno). El Resource Server no
cambia.

```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,
)
```

## Notas de seguridad

<Warning>
  El JWT intercambiado transporta los permisos **completos** del usuario en B Brands (sin
  least-privilege por cliente). Esto es aceptable para clientes first-party
  confiables. Si se requiere acotación, introdúcela en el exchange (limita el
  conjunto de permisos por cliente).
</Warning>

* **El TTL corto es obligatorio** (por defecto 900s). Nunca emitas el token de login
  tradicional de larga duración a través de este flujo.
* El token es **revocable** vía la blacklist por `jti`. Tanto este endpoint como
  el autorizador interno rechazan los tokens en blacklist.
* **No hay refresh** para el token interno por diseño. Cuando expira, el
  cliente vuelve a ejecutar el exchange con un token OIDC aún válido.

## Máquina a máquina (sin usuario)

Para procesos sin un usuario interactivo (p. ej. un cron de sincronización), **no** uses OAuth
ni este exchange. Usa el modelo de **API key** existente de B Brands, aprovisionado con
least privilege. Consulta [Integración de clientes](/es/oauth/client-integration).
