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

# Endpoints OAuth / OIDC

> Contrato de los endpoints estándar OAuth 2.1 / OpenID Connect: discovery, JWKS, authorize, consent, token, userinfo y revoke.

Todos los endpoints viven bajo el origen del issuer canónico `auth.*` del entorno. Ningún
endpoint de cara al cliente apunta al motor subyacente — incluso `/.well-known/*`
se sirve desde el issuer. Los ejemplos a continuación usan la URL base de producción
`https://auth.bbrands.io`; sustituye por la base de cada entorno:

| Entorno       | URL base / issuer                       |
| ------------- | --------------------------------------- |
| Development   | `https://development.auth.bbrands.io`   |
| Certification | `https://certification.auth.bbrands.io` |
| Production    | `https://auth.bbrands.io`               |

## Convenciones

* **Content-Type**: `application/json`, excepto los endpoints OAuth que usan
  `application/x-www-form-urlencoded`.
* **Transporte**: solo HTTPS (TLS 1.2+).
* **Caché**: `Cache-Control: no-store` en cada endpoint OAuth.
* **Errores**: los endpoints OAuth devuelven `{"error": "...", "error_description": "..."}`.
* **Correlación**: cada respuesta transporta `X-Request-Id`.

## Discovery

### GET `/.well-known/openid-configuration`

Devuelve el documento de discovery OIDC. **Público, sin auth.** Apunta la
autoconfiguración de tu cliente a esta URL.

<CodeGroup>
  ```json Response 200 theme={null}
  {
    "issuer": "https://auth.bbrands.io",
    "authorization_endpoint": "https://auth.bbrands.io/api/v3/oauth/authorize",
    "token_endpoint": "https://auth.bbrands.io/api/v3/oauth/token",
    "userinfo_endpoint": "https://auth.bbrands.io/api/v3/oauth/userinfo",
    "revocation_endpoint": "https://auth.bbrands.io/api/v3/oauth/revoke",
    "jwks_uri": "https://auth.bbrands.io/.well-known/jwks.json",
    "response_types_supported": ["code"],
    "grant_types_supported": [
      "authorization_code",
      "refresh_token",
      "urn:ietf:params:oauth:grant-type:token-exchange"
    ],
    "scopes_supported": ["email", "openid", "profile"],
    "subject_types_supported": ["public"],
    "id_token_signing_alg_values_supported": ["ES256", "RS256"],
    "token_endpoint_auth_methods_supported": [
      "client_secret_basic",
      "client_secret_post"
    ]
  }
  ```
</CodeGroup>

<Note>
  El `iss` en el discovery y en cada token de acceso emitido es la URL canónica
  `auth.*` por entorno — nunca la URL del motor subyacente. El
  `registration_endpoint` intencionalmente **no** se expone: el registro dinámico
  de clientes está desactivado; los clientes se dan de alta a través de la consola de administración de identidad.
</Note>

<Note>
  `id_token_signing_alg_values_supported` lista `["ES256"]` por defecto y
  `["ES256", "RS256"]` en los entornos donde la vía RS256 del id\_token está
  habilitada. ES256 es siempre el algoritmo por defecto; RS256 solo aplica a
  los clientes registrados con `id_token_alg: "RS256"` (p. ej. Odoo).
</Note>

### GET `/.well-known/jwks.json`

Devuelve las claves públicas para validar los JWT OAuth. **Público.** Servido por el
IdP (proxy/caché del JWKS del motor); el cliente nunca ve la URL del motor.

```bash Response headers theme={null}
Cache-Control: public, max-age=3600
ETag: "<hash>"
```

<Warning>
  JWKS aplica solo a los tokens **OAuth**. El JWT interno HS256 de B Brands se
  valida con un secreto simétrico del lado del servidor y nunca se expone aquí.
</Warning>

<Note>
  En los entornos con la vía RS256 habilitada, el documento contiene la clave
  EC P-256 del motor **más** la clave pública RSA del IdP (ambas con `kid`).
  Los verificadores resuelven las claves por `kid`, así que la clave extra es
  transparente para los clientes ES256.
</Note>

## Endpoint de autorización

### GET `/api/v3/oauth/authorize`

Inicia el flujo de authorization-code. **Llamado por el navegador del usuario** tras una
redirección desde el cliente. La solicitud se valida estrictamente, se reenvía al
motor, y el navegador se redirige a la UI de consentimiento en el mismo origen `auth.*`.

<ParamField query="response_type" type="string" required>
  Debe ser `code`.
</ParamField>

<ParamField query="client_id" type="string" required>
  El identificador del cliente OAuth registrado (UUID provisto en el onboarding).
</ParamField>

<ParamField query="redirect_uri" type="string" required>
  Debe coincidir exactamente con una de las redirect URIs registradas del cliente.
</ParamField>

<ParamField query="scope" type="string" required>
  Lista separada por espacios. Debe incluir `openid`. Soportados: `openid email profile`.
</ParamField>

<ParamField query="state" type="string" required>
  Valor anti-CSRF del cliente, devuelto en la redirección.
</ParamField>

<ParamField query="code_challenge" type="string" required>
  Challenge PKCE. **PKCE es obligatorio** para cada cliente.
</ParamField>

<ParamField query="code_challenge_method" type="string" required>
  Debe ser `S256` (`plain` se rechaza).
</ParamField>

<ParamField query="nonce" type="string">
  Recomendado. Valor anti-replay del ID-token.
</ParamField>

**Respuesta**: redirección `302` a `https://auth.bbrands.io/oauth/consent?...` (la
UI de consentimiento). Con una sesión válida y un cliente first-party habilitado, el consentimiento
se autoaprueba y el navegador se redirige de vuelta al cliente con
`?code&state`.

**Los errores de validación** se devuelven como JSON `400` (sin redirección, ya que el
`redirect_uri` no puede considerarse confiable antes de la validación):

| `error`               | Causa                                                                                                                                       |
| --------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
| `invalid_request`     | Parámetro faltante/mal formado (sin PKCE, `scope` sin `openid`, challenge no `S256`, `state` faltante, …) o un `redirect_uri` no registrado |
| `unauthorized_client` | `client_id` desconocido, suspendido o revocado                                                                                              |

## Consentimiento

### UI de consentimiento — `https://auth.bbrands.io/oauth/consent`

Servida por la app de autenticación (no una API). Valida la sesión de B Brands
(redirigiendo al login con `?next=` cuando falta), muestra el nombre del cliente y los
scopes OIDC solicitados, y ofrece Permitir / Denegar. Los usuarios `first_party` + habilitados se
autoaprueban.

### POST `/api/v3/oauth/decision`

Registra la decisión de consentimiento del usuario para **auditoría**. **No** continúa el
flujo de código OAuth (el motor lo hace, vía el `authorize_url`).

<ParamField body="client_handle" type="string" required>
  Handle estable del cliente, p. ej. `erp-prod`.
</ParamField>

<ParamField body="decision" type="string" required>
  `approve` o `deny`.
</ParamField>

<ParamField body="scopes" type="string[]">
  Scopes solicitados.
</ParamField>

<ParamField body="user" type="string">
  Identificador del usuario.
</ParamField>

<CodeGroup>
  ```json Response 200 theme={null}
  {
    "message": "Decision recorded",
    "data": { "decision": "approve" },
    "meta": { "correlation": "...", "status": 200, "timestamp": "..." }
  }
  ```
</CodeGroup>

Un `client_handle` faltante o un `decision` inválido devuelve `400`.

## Endpoint de token

### POST `/api/v3/oauth/token`

Intercambia un código de autorización por tokens, o refresca un token de acceso. **Llamado
servidor a servidor** por el cliente. El gateway hace proxy de la solicitud al motor,
que ejecuta el hook de token de acceso (verificación de habilitación + reescritura de `iss`/`aud`).

**Headers**: `Content-Type: application/x-www-form-urlencoded` y
`Authorization: Basic <base64(client_id:client_secret)>` (o el secreto en el
cuerpo para `client_secret_post`).

<CodeGroup>
  ```bash authorization_code theme={null}
  grant_type=authorization_code
  code=<code>
  redirect_uri=<uri>
  client_id=<id>
  code_verifier=<pkce_verifier>
  ```

  ```bash refresh_token theme={null}
  grant_type=refresh_token
  refresh_token=<token>
  client_id=<id>
  ```
</CodeGroup>

<CodeGroup>
  ```json Response 200 theme={null}
  {
    "access_token": "<ES256 identity JWT>",
    "token_type": "bearer",
    "expires_in": 86400,
    "refresh_token": "<rotated token>",
    "id_token": "<ES256 identity JWT>"
  }
  ```
</CodeGroup>

El **token de acceso** transporta los claims de identidad canónicos — valídalo contra
el issuer y el JWKS:

```json theme={null}
{
  "iss": "https://auth.bbrands.io",
  "aud": "bbrands-idp",
  "sub": "<user uuid>",
  "email": "user@example.com",
  "jti": "<uuid>",
  "client_id": "<your client_id>",
  "https://bbrands.io/client_handle": "<client_handle, e.g. erp-prod>",
  "https://bbrands.io/platform_type": "<platform_type, e.g. erp>"
}
```

<Note>
  `aud` es la audiencia fija del IdP `bbrands-idp`; el cliente solicitante se
  identifica por `https://bbrands.io/client_handle` (y `client_id`). Valida
  `iss`, la firma ES256 vía JWKS, y que `client_handle` coincida con tu
  propio handle. El `id_token` conserva el issuer/audience del motor y está pensado
  para la verificación de `nonce` y los claims de perfil; usa el **token de acceso** como la
  aserción de identidad.
</Note>

<Note>
  **Vía RS256 del id\_token**: los clientes registrados con
  `id_token_alg: "RS256"` (p. ej. Odoo con el módulo OCA `auth_oidc`) reciben
  el mismo `id_token` re-firmado con la clave RSA del IdP — header
  `alg: RS256` con un `kid` que resuelve vía `/.well-known/jwks.json`, con
  todos los claims preservados (`nonce`, `at_hash`, `exp`, ...). El **token de
  acceso siempre es ES256**.
</Note>

**Errores típicos** (mapeo OAuth):

| HTTP | `error`          | Causa                                                             |
| ---- | ---------------- | ----------------------------------------------------------------- |
| 400  | `invalid_grant`  | Código expirado, refresh inválido, o usuario no habilitado (hook) |
| 401  | `invalid_client` | `client_secret` incorrecto                                        |
| 403  | `access_denied`  | Usuario deshabilitado entre emisiones                             |

## Endpoint UserInfo

### GET `/api/v3/oauth/userinfo`

Devuelve los claims de **identidad** del usuario, enriquecidos con la dimensión
de cuenta de negocio. Con proxy al userinfo del motor.

**Headers**: `Authorization: Bearer <access_token>` (obligatorio; un header faltante
devuelve `401 invalid_token` con `WWW-Authenticate: Bearer`).

<CodeGroup>
  ```json Response 200 theme={null}
  {
    "sub": "uuid-of-user",
    "email": "user@example.com",
    "email_verified": true,
    "name": "Jane Doe",
    "given_name": "Jane",
    "family_name": "Doe",
    "preferred_username": "jdoe",
    "https://bbrands.io/profile": {
      "account": "uuid-of-account",
      "odoo": "ODOO-84213",
      "user": "uuid-of-user"
    }
  }
  ```
</CodeGroup>

<Note>
  Sin claim de permisos/roles. UserInfo es identidad más contexto de tenancy:
  el objeto `https://bbrands.io/profile` lleva la cuenta vinculada al usuario
  mediante el rol `primary_user` (`account`), el id del sistema externo de esa
  cuenta (`odoo`, `null` hasta que la API de migración la mapee) y el sujeto
  (`user`, igual a `sub`).
</Note>

## Endpoint de revocación

### POST `/api/v3/oauth/revoke`

Invalida un token (RFC 7009), típicamente al cerrar sesión. Agrega el `jti` del token al
store de blacklist local y hace proxy de la revocación al motor.

**Body** (`application/x-www-form-urlencoded`): `token`, `token_type_hint`
(`access_token` | `refresh_token`), `client_id`.

<CodeGroup>
  ```json Response 200 theme={null}
  { "revoked": true }
  ```
</CodeGroup>

<Note>
  Según la RFC 7009 la respuesta es siempre `200` y opaca, independientemente de si el
  token existía.
</Note>

## Códigos de error personalizados

Más allá de los errores OAuth estándar, los endpoints específicos de B Brands pueden devolver estos en
`error.code`:

| Código                                | HTTP | Significado                                  |
| ------------------------------------- | ---- | -------------------------------------------- |
| `bbrands.client_not_active`           | 400  | Cliente suspendido o revocado                |
| `bbrands.user_not_provisioned`        | 403  | Usuario no habilitado para esa plataforma    |
| `bbrands.token_blacklisted`           | 401  | Token revocado individualmente               |
| `bbrands.client_secret_grace_expired` | 401  | Secreto rotado, período de gracia finalizado |
| `bbrands.invalid_subject_token`       | 400  | Subject token inválido en el exchange        |

## Rate limiting

| Endpoint                   | Límite      | Granularidad          |
| -------------------------- | ----------- | --------------------- |
| `/oauth/authorize`         | 30 req/min  | IP                    |
| `/oauth/token` (auth code) | 10 req/min  | IP + client\_id       |
| `/oauth/token` (refresh)   | 60 req/min  | client\_id + user\_id |
| `/oauth/revoke`            | 30 req/min  | client\_id            |
| `/oauth/exchange`          | 60 req/min  | client\_id + user\_id |
| `/idp/*` (management)      | 120 req/min | user\_id (admin)      |

Las respuestas incluyen `X-RateLimit-Limit`, `X-RateLimit-Remaining`,
`X-RateLimit-Reset`; `429` transporta `Retry-After`.
