# Cambios de OAuth

Historial de cambios del servidor OAuth 2.1 de UTILIA OS y de lo que lo rodea: cambios incompatibles, retiradas con fecha, novedades y refuerzos de seguridad.

Esta página reúne los cambios del servidor OAuth 2.1 de UTILIA OS que afectan a quien ya tiene una integración: qué cambió, desde cuándo y qué tienes que adaptar. Las entradas van de la más reciente a la más antigua. Las versiones exactas de los SDK están en el `CHANGELOG.md` de cada paquete y en [Versiones y compatibilidad del SDK](https://os.utilia.ai/docs/developers/sdk/versions).

## Retiradas pendientes

**Próximas retiradas**

1. **16 oct 2026**: Endpoint antiguo de permisos. GET /api/oauth/user-scopes pasa a GET /api/oauth/user/scopes. Ya pasó su fecha y puede dejar de responder en cualquier momento.
2. **1 feb 2027**: Campo scope de /api/oauth/apps. Se sustituye por el campo permissions.
3. **2 mar 2027**: Fin de las transiciones de scopes. Los tokens anteriores al 2 de septiembre de 2026 dejan de leer datos sensibles y personas de contacto con los scopes antiguos.

`GET /api/oauth/user-scopes` responde con las cabeceras `Deprecation: true`, `Sunset: Thu, 16 Oct 2026 00:00:00 GMT` y `Link` al sustituto. Si las ves en tus registros, tienes código que todavía llama a un endpoint antiguo. `POST /api/oauth/apps/revoke` se retiró el 20 de octubre de 2026 y ya no responde: usa `POST /api/oauth/apps/:clientId/revoke`.

## Estado actual

| Elemento | Valor |
| - | - |
| Access token | 1 hora (3.600 s) |
| Refresh token | 90 días desde la autorización; rota en cada renovación |
| Código de autorización | 60 segundos, un solo uso |
| PKCE | Obligatorio, solo `S256` |
| SDK oficiales | `@utilia-os/sdk-js` y `utilia-sdk`, versión 4.1.0 |

---

## 20 de octubre de 2026

- **Se retira la dirección única `/api/mcp`.** La dirección que reunía todas las áreas en un solo servidor MCP deja de funcionar: `POST`, `GET` y `DELETE` a `https://os.utilia.ai/api/mcp` responden 410 con un error JSON-RPC que enumera la dirección de cada área. Conecta tu asistente a la del área que uses: `/api/mcp/crm`, `/api/mcp/work`, `/api/mcp/finance`, `/api/mcp/marketing`, `/api/mcp/chat` o `/api/mcp/rrhh`. Las direcciones de área no cambian.
- **OAuth deja de reconocerla como recurso.** Una autorización con `resource=/api/mcp` responde `invalid_request`, y los metadatos generales de [descubrimiento](https://os.utilia.ai/docs/developers/oauth/discovery) ya no la anuncian. Los tokens emitidos para ella reciben 401 `invalid_token` en todos los servidores, para que el cliente vuelva a autorizarse con la dirección del área, y ya no se renuevan.
- **Una autorización sin `resource` solo concede identidad.** Hasta ahora se ataba a `/api/mcp`. Si pides scopes de un servidor MCP sin indicar su dirección, y el registro de tu cliente no tiene un recurso por defecto, la autorización responde `invalid_request` con las direcciones de las áreas. Iniciar sesión con UTILIA (`openid`, `profile` y `email`) sigue igual.

---

## 13 de octubre de 2026

- **Formación, scrapers, gamificación y anuncios internos respetan lo que se autorizó.** Hasta ahora, con un token de solo lectura del CRM o de Gestión de proyectos se podían crear, publicar y borrar formaciones, lanzar y borrar scrapers, ajustar puntos de gamificación y publicar anuncios internos, siempre que la persona tuviera el permiso. Ahora consultar sigue funcionando con cualquier scope de lectura, crear y modificar exige un scope de modificación (`crm:write`, `dev:write` o un `:update`), y eliminar, un scope de eliminación (`crm:full`, `dev:full` o un `:delete`). Si tu integración borra en estos módulos con `crm:write` o `dev:write`, pide también el `:full` o un `:delete`. El directorio de usuarios (`users`) también comprueba el scope en cada consulta; sigue funcionando con los mismos scopes con los que hoy ves la herramienta. Detalle en [Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources#formación-scrapers-gamificación-y-anuncios-internos).

---

## 12 de octubre de 2026

- **Todo `invalid_client` lleva `error_uri`.** La respuesta añade `error_uri`, la dirección del apartado `invalid_client` de [Problemas frecuentes con OAuth](https://os.utilia.ai/docs/developers/oauth/troubleshooting#invalid_client) en la documentación pública: la misma en todos los casos y en todos los endpoints. Lleva a la explicación de las causas posibles sin decir cuál ha sido la tuya.
- **`Missing client_id` desaparece como texto propio.** Una petición sin `client_id` en `POST /api/oauth/token` o `POST /api/oauth/introspect` responde como cualquier otro fallo de autenticación del cliente: `401` con `Client authentication failed`.
- **Cabecera `WWW-Authenticate` en el 401 de autenticación del cliente.** Si tu cliente se autenticó con la cabecera `Authorization: Basic`, el `401 invalid_client` de esos dos endpoints lleva `WWW-Authenticate: Basic realm="UTILIA OS"`, como pide la RFC 6749 (sección 5.2). Si mandas las credenciales en el cuerpo, no la lleva.
- **La introspección rechazada deja rastro.** Si un cliente de una aplicación externa de tu organización intenta la introspección y se le rechaza, lo verás en **Actividad › Errores**, en la ficha de esa aplicación, como mucho una vez cada 24 horas. El texto es siempre el mismo y no dice la causa concreta.

---

## 9 de octubre de 2026

### Cambios incompatibles

**La introspección (`POST /api/oauth/introspect`) tiene dos restricciones nuevas:**

1. **Solo la usan los clientes confidenciales.** El cliente tiene que autenticarse con su secreto (`client_secret_basic` o `client_secret_post`). Un cliente público, sin secreto, recibe `401 invalid_client`.
2. **Solo responde sobre los tokens de ese mismo cliente.** Un token emitido a otro cliente responde `active: false`, aunque siga vivo.

**Qué hacer:** si tu servidor comprueba tokens con la introspección, usa un cliente de tipo **Con servidor propio** y manda su secreto. Una aplicación pública (de navegador, móvil o escritorio) no necesita la introspección: llama a la API con el access token y, si responde `401`, renuévalo con el refresh token. Detalle en [Canje, revocación e introspección de tokens](https://os.utilia.ai/docs/developers/oauth/token-revoke-introspect).

### Más cambios

- **Un cliente desactivado invalida sus tokens.** Al desactivar OAuth en una aplicación, sus access tokens dejan de valer en el acto, aunque no consten revocados: la introspección responde `active: false` y la renovación falla con `invalid_client`.
- **`invalid_client` ya no dice qué falló.** En `POST /api/oauth/token` y `POST /api/oauth/introspect`, un cliente inexistente, desactivado o vencido, un secreto que falta o que no es correcto y un cliente público en la introspección responden lo mismo: `401` con `Client authentication failed`. Antes cada caso tenía su texto (`Client not found`, `Client is inactive`, `Client secret required`...) y con ellos se podía averiguar qué clientes existen. Si tu integración leía esos textos, compara solo el campo `error`. Las causas posibles están en [Problemas frecuentes con OAuth](https://os.utilia.ai/docs/developers/oauth/troubleshooting#invalid_client).
- **El cierre de sesión exige un `id_token_hint` firmado por UTILIA OS.** Se comprueba su firma RS256 con la clave pública de `/api/oauth/jwks` y el claim `iss` es obligatorio.
- **La marca de aplicación propia y la de «Verificada por UTILIA» solo las asigna UTILIA OS.** Si pides `is_first_party` o `verified` con valor `true`, recibes un `403`.

---

## 5 de octubre de 2026

### La persona puede conceder menos scopes de los que pides

La pantalla de consentimiento permite desactivar grupos de permisos antes de autorizar. Los de identidad (`openid`, `profile`, `email`) no se pueden desactivar. El token lleva solo lo que quedó activado, y las renovaciones conservan ese conjunto reducido.

Si tu aplicación no manda `scope`, la pantalla enseña ahora el conjunto por defecto que se concede (los scopes permitidos del registro del cliente o, si no tiene, los de identidad), y la persona también puede estrecharlo. Antes ese conjunto se concedía sin enseñarse.

**Qué hacer:** lee el campo `scope` de la respuesta de `POST /api/oauth/token` y desactiva lo que dependa de un scope no concedido. Si con la selección un servidor MCP se quedara sin ningún scope útil, la pantalla no deja autorizar, así que nunca se emite un token que el servidor rechazaría. Detalle en [Pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen).

### Compartir documentación con enlace tiene scope propio

Crear un enlace público a un documento exige ahora `dev:docs:share`, que no viaja en ningún conjunto. Antes bastaba con `dev:docs:update`. Los tokens anteriores no lo llevan y tienen que volver a autorizarse.

---

## 14 de septiembre de 2026

El límite de los documentos de descubrimiento (`/.well-known/*`) sube de 120 a 600 peticiones por minuto y dirección IP: un cliente MCP con varios servidores configurados los pide muchas veces al arrancar.

---

## 3 de septiembre de 2026

- **Fusiones con scope propio**: `crm:clients:merge`, `crm:contacts:merge` y `crm:leads:merge` no viajan en ningún conjunto, tampoco en `crm:full`. Se piden por su nombre.
- **Acceso al portal del cliente con scope propio**: la herramienta `portal_access` exige `crm:portal:view` o `crm:portal:manage` y ya no se abre con `crm:contacts:update`, sin periodo de transición.

## 2 de septiembre de 2026

- **Datos sensibles con scope propio**: el NIF, el límite de crédito y el descuento del cliente pasan a `crm:clients:view-sensitive`, y los datos bancarios y fiscales del proveedor, a `finance:suppliers:view-sensitive`. Los tokens anteriores siguen leyéndolos con el scope antiguo hasta el 2 de marzo de 2027.
- **Personas de contacto de los proveedores**: pasan a `finance:supplier-contacts:view` y `finance:supplier-contacts:manage`, fuera de `finance:read` y `finance:write`. Los tokens anteriores siguen operando con `finance:suppliers:view` y `finance:suppliers:update` hasta el 2 de marzo de 2027.

---

## 28 de agosto de 2026

Ningún conjunto concede ya la administración de la organización: `org:team:*`, `org:observability:*` y `org:settings:update` se piden por su nombre. Hasta esta fecha los expandía `crm:full`. Solo `org:settings:view` sigue viajando en `crm:read` y `crm:full`.

---

## 28 de julio de 2026

Cambios en el alcance de los tokens y en la respuesta de `GET /api/oauth/apps`.

### El campo `scope` de `GET /api/oauth/apps` se retira el 1 de febrero de 2027

Sigue llegando hasta esa fecha, pero ha cambiado de significado: ya no lista todo lo que hay en el token, sino solo lo que el plan de la organización deja ejercer. Lo demás no se ha revocado: sigue concedido, pero pertenece a aplicativos no contratados y el servidor MCP lo deniega.

Campos nuevos para sustituirlo:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `permissions` | `array` | No | Permisos ejercibles con el plan actual. Cada elemento: scope, description (en castellano) e impact (destructive, write, read o identity). |
| `permissionCount` | `number` | No | Número de permisos ejercibles. Siempre igual a la longitud de permissions. |
| `unavailablePermissionCount` | `number` | No | Permisos concedidos que el plan actual no deja ejercer. |
| `unavailableModules` | `array` | No | Aplicativos no contratados de los que vienen esos permisos. Cada elemento: key y label. |

La forma completa de la respuesta está en [OAuth: visión general](https://os.utilia.ai/docs/developers/oauth/overview).

### Una renovación solo puede estrechar el alcance

Antes, `grant_type=refresh_token` recalculaba el alcance a partir de los permisos vigentes de la persona, así que un token podía crecer más allá de lo que se consintió. Ahora el token renovado es siempre un subconjunto del consentido, acotado además por el servidor al que está atado. Para ampliar el alcance hay que volver a autorizar.

Si la persona pierde todos los permisos del servidor del token, la renovación revoca la cadena y responde `invalid_grant`. Los tokens solo de identidad (`openid`, `profile`, `email`) no se ven afectados.

Los tokens emitidos antes del cambio se revocaron al aplicarlo, salvo los solo de identidad: su primera renovación respondió `invalid_grant` y hubo que autorizar una vez más.

**Qué hacer:**

- Sustituye `scope` por `permissions` antes del 1 de febrero de 2027, y muestra también `unavailablePermissionCount` y `unavailableModules`, que explican por qué faltan permisos.
- Trata `invalid_grant` volviendo a pedir la autorización.
- No cuentes con que un token gane scopes solo: pídelos en una autorización nueva.

---

## 12 de mayo de 2026

- **`support:write` incluye `support:tickets:create`**: los clientes con `support:write` pueden abrir tickets sin volver a autorizar, porque el conjunto se resuelve en el servidor. Si no quieres esa ampliación, revoca el token y pide solo los scopes individuales que necesites.
- **Abrir tickets por MCP**: la herramienta `tickets` admite `create`. Quien abre el ticket es siempre la persona dueña del token, el título y la descripción se limpian de HTML, y hay un máximo de 10 tickets por hora y persona por esta vía.

---

## 16 de abril de 2026

### Cambios incompatibles

- **El access token dura 1 hora** en lugar de 30 días. Toda integración tiene que renovar con el refresh token; el valor exacto llega en `expires_in`.
- **El refresh token rota en cada renovación**. Reutilizar uno ya rotado revoca la cadena y responde `invalid_grant` (`Refresh token has been rotated`). Si dos renovaciones compiten a la vez por el mismo, la que pierde recibe `Refresh token rotated concurrently`. Detalle en [Rotación del refresh token](https://os.utilia.ai/docs/developers/oauth/refresh-token-rotation).

### Endpoints que se retiran el 16 de octubre de 2026

- `GET /api/oauth/user-scopes` pasa a `GET /api/oauth/user/scopes`, que además admite `client_id` para devolver la información pública del cliente.
- `POST /api/oauth/apps/revoke` pasa a `POST /api/oauth/apps/:clientId/revoke`, con el identificador en la ruta y sin cuerpo.

### Novedades

- `GET /api/oauth/clients/:clientId`: información pública de un cliente. Responde `{ "known": false }` con `200` si no existe, para impedir enumerar clientes.
- `GET` y `POST /api/oauth/logout`: cierre de sesión iniciado por tu aplicación (OpenID Connect RP-Initiated Logout 1.0), con el campo `post_logout_redirect_uris` en el registro de clientes (hasta 10 direcciones).
- Parámetros `prompt` (`none`, `login`, `select_account`, `consent`) y `login_hint` en `/api/oauth/authorize`.

### Seguridad

- Las direcciones de los clientes (`redirect_uris`, `client_uri`, `logo_uri`, `post_logout_redirect_uris`) solo admiten `http` y `https` y rechazan los hosts internos. `localhost`, `127.0.0.0/8` y `::1` se admiten en las direcciones de vuelta, porque los clientes nativos las necesitan.
- `login_hint` solo admite letras, dígitos y `. _ @ + -`, con un máximo de 255 caracteres.
- Los tokens de una persona cuya cuenta deja de estar activa se rechazan de inmediato.
- `POST /api/oauth/introspect` exige autenticación de cliente.

### Límites de uso establecidos

| Endpoint | Límite |
| - | - |
| `GET /.well-known/*` | 120 por minuto (600 desde el 14 de septiembre de 2026) |
| `POST /api/oauth/register` | 30 por hora |
| `POST /api/oauth/token` y `POST /api/oauth/revoke` | 60 por minuto |
| `POST /api/oauth/introspect` | 120 por minuto |
| `GET /api/oauth/scopes` | 60 por minuto |
| `GET /api/oauth/clients/:clientId` y `/api/oauth/logout` | 30 por minuto |

---

## 8 de abril de 2026

Los SDK admiten el parámetro `theme` (`light` o `dark`) al construir la URL de autorización.

---

## 2 de abril de 2026

- **OAuth en las aplicaciones externas**: se activa desde la ficha de cada aplicación, en Gestión de proyectos › Integraciones.
- **OpenID Connect**: `/.well-known/openid-configuration`, `/api/oauth/userinfo`, `/api/oauth/jwks` e `id_token` firmados con los claims estándar.
- **SDK**: `getAuthorizationUrl()` y `get_authorization_url()` gestionan PKCE solos; `handleCallback(code)` y `handle_callback(code)` recuperan el `code_verifier` y comprueban el `state`; `isAuthenticated()`, `logout()` y, en JavaScript, `loginWithPopup()`.
