# OAuth: visión general

Qué te permite hacer el servidor OAuth 2.1 y OpenID Connect de UTILIA OS, qué endpoints expone y cuánto duran sus tokens.

UTILIA OS incluye un servidor de autorización OAuth 2.1 con OpenID Connect. Sirve para dos cosas: que las personas entren en tu aplicación con su cuenta de UTILIA OS (el botón «Iniciar sesión con UTILIA») y que un cliente, como un asistente de IA, trabaje con sus datos a través de los servidores MCP de UTILIA OS, siempre con su consentimiento y nunca más allá de sus permisos ni del plan de su organización.

## Qué puedes hacer con un token

El access token que obtienes al final del flujo actúa en nombre de una persona y no sirve para todo. No lo confundas con la clave de API de tu aplicación:

**Token OAuth o clave de API**

### Access token OAuth (En nombre de una persona)

Identifica a la persona con el id_token y GET /api/oauth/userinfo, y llama en su nombre a los servidores MCP de UTILIA OS (CRM, Proyectos, Finanzas, Marketing, Chat y RRHH). Va atado al servidor que indicas en resource y solo lleva sus scopes.

### Clave de API (En nombre de tu aplicación)

Llama a la API REST externa (/api/external/v1/...) con la cabecera X-Api-Key. Es la que usan los servicios REST de los SDK. No sirve para identificar a nadie.

Las direcciones de los servidores MCP están en [Copiloto y MCP](https://os.utilia.ai/docs/developers/mcp/quickstart), y la clave de API, en [Autenticación con clave de acceso](https://os.utilia.ai/docs/developers/getting-started/authentication).

## Quién puede autorizar tu aplicación

Depende de cómo nace el cliente. Uno creado desde la ficha de una aplicación de tu organización solo lo autorizan personas de tu organización. Uno creado con el registro dinámico (`POST /api/oauth/register`) no es de ninguna organización: lo autoriza cualquier persona con cuenta en UTILIA OS, y el token trabaja con los datos de la organización de esa persona.

Si tu aplicación no es para todo el mundo, filtra tú quién entra: el claim `org_id` dice de qué organización es la persona, y cada cuenta se identifica por `sub`, nunca por el correo. Lo explica [Quién puede autorizar tu aplicación](https://os.utilia.ai/docs/developers/oauth/apps-panel#quién-puede-autorizar-tu-aplicación).

## Cómo funciona el flujo

**Ciclo de una integración OAuth**

1. [**Registrar el cliente**](https://os.utilia.ai/docs/developers/oauth/apps-panel) (Panel o /api/oauth/register): Obtienes el client_id y, con servidor propio, la clave secreta.
2. [**Autorizar**](https://os.utilia.ai/docs/developers/oauth/authorization-flow) (/api/oauth/authorize): La persona inicia sesión y decide qué concede; vuelves con un código.
3. [**Canjear el código**](https://os.utilia.ai/docs/developers/oauth/token-revoke-introspect) (/api/oauth/token): Recibes access_token, refresh_token e id_token.
4. **Usar el token** (userinfo o servidor MCP): Durante 1 hora, en la cabecera Authorization: Bearer.
5. [**Renovar**](https://os.utilia.ai/docs/developers/oauth/refresh-token-rotation) (/api/oauth/token): Con el refresh token, que cambia en cada renovación.
6. **Revocar** (/api/oauth/revoke): Al cerrar sesión, para que los tokens dejen de valer.

_El registro se hace una vez; la autorización, una vez por persona; la renovación, cada hora mientras dure la cadena._

Los clientes MCP descubren antes las direcciones del servidor en `/.well-known/...`. Solo se admite el flujo de código de autorización (`response_type=code`) con PKCE obligatorio y método `S256`. No hay flujo implícito, ni híbrido, ni `client_credentials`, ni `device_code`.

> **Atención: Endpoints antiguos de OAuth**
>
> `POST /api/oauth/apps/revoke` se retiró el 20 de octubre de 2026 y ya no responde: usa `POST /api/oauth/apps/:clientId/revoke`. `GET /api/oauth/user-scopes` sigue respondiendo con las cabeceras `Deprecation: true` y `Sunset: Thu, 16 Oct 2026 00:00:00 GMT`, pero ya pasó su fecha: cámbialo cuanto antes por `GET /api/oauth/user/scopes`. Detalle en [Cambios de OAuth](https://os.utilia.ai/docs/developers/oauth/changelog).

## Estándares implementados

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `RFC 6749` | `/api/oauth/authorize, /api/oauth/token` | No | Marco OAuth 2.0: autorización y emisión de tokens. |
| `RFC 7636` | `code_challenge` | No | PKCE, obligatorio en todos los flujos y solo con S256. |
| `RFC 7591` | `/api/oauth/register` | No | Registro dinámico de clientes, sin intervención manual. |
| `RFC 7009` | `/api/oauth/revoke` | No | Revocación de access tokens y refresh tokens. |
| `RFC 7662` | `/api/oauth/introspect` | No | Introspección: estado y metadatos de un access token. |
| `RFC 8414` | `/.well-known/oauth-authorization-server` | No | Metadatos del servidor de autorización. |
| `RFC 8707` | `resource` | No | Indicadores de recurso: el token queda atado a un servidor MCP concreto. |
| `RFC 9728` | `/.well-known/oauth-protected-resource` | No | Metadatos de cada recurso protegido y de sus scopes. |
| `OpenID Connect Core 1.0` | `/api/oauth/userinfo, /api/oauth/jwks` | No | id_token firmado con RS256 y datos de la persona. |

## Endpoints

### Descubrimiento (públicos)

No necesitan ninguna credencial. Viven en la raíz del dominio, fuera de `/api`. El detalle de cada uno está en [Descubrimiento del servidor](https://os.utilia.ai/docs/developers/oauth/discovery).

- `GET /.well-known/oauth-authorization-server`: metadatos del servidor de autorización.
- `GET /.well-known/openid-configuration`: los mismos, ampliados con los campos de OpenID Connect.
- `GET /.well-known/oauth-protected-resource`: metadatos de un recurso; con `?resource=` devuelve los del servidor MCP indicado.
- `GET /.well-known/mcp-crm.json`, `mcp-work.json` (y su alias `mcp-dev.json`), `mcp-finance.json`, `mcp-marketing.json`, `mcp-chat.json` y `mcp-rrhh.json`: atajos con los metadatos de cada servidor MCP.
- `GET /.well-known/jwks.json`: claves públicas para verificar el `id_token`.
- `GET /.well-known/resource-server.json`: variante antigua de `oauth-protected-resource`, solo por compatibilidad.

### Flujo de autorización

- `GET /api/oauth/authorize`: inicia la solicitud y lleva a la persona a la pantalla de consentimiento.
- `POST /api/oauth/authorize`: registra la decisión de la persona. Solo lo llama la propia pantalla de consentimiento.
- `POST /api/oauth/token`: canjea el código por tokens o renueva con el refresh token.
- `POST /api/oauth/register`: registro dinámico de clientes.

### Ciclo de vida de los tokens

- `POST /api/oauth/revoke`: revoca un token. Responde siempre `200`.
- `POST /api/oauth/introspect`: devuelve el estado de un access token emitido a tu cliente o `{ "active": false }`. Solo para clientes confidenciales autenticados con su secreto; un cliente público no puede usarlo.

### OpenID Connect

- `GET` y `POST /api/oauth/userinfo`: datos de la persona. Exige un access token con el scope `openid`.
- `GET /api/oauth/jwks`: claves públicas (las mismas que `/.well-known/jwks.json`).
- `GET` y `POST /api/oauth/logout`: cierre de sesión iniciado por tu aplicación. Detalle en [Cierre de sesión desde tu aplicación](https://os.utilia.ai/docs/developers/oauth/logout).

### Consulta pública

- `GET /api/oauth/clients/:clientId`: dice si un cliente existe y si es una aplicación propia de UTILIA OS. Si no existe responde `{ "known": false }` con `200`, para que nadie pueda enumerar clientes.
- `GET /api/oauth/scopes?scopes=...`: descripción en castellano de los scopes que indiques.

### Aplicaciones de la propia persona (sesión de UTILIA OS)

Estos endpoints los usa la interfaz de UTILIA OS con la sesión de la persona; son los que hay detrás de Mi perfil › IA y aplicaciones.

- `GET /api/oauth/apps`: aplicaciones que la persona ha autorizado, con los permisos que cada una puede ejercer de verdad.
- `POST /api/oauth/apps/:clientId/revoke`: retira el acceso de una aplicación. No lleva cuerpo.
- `GET /api/oauth/user/scopes`: scopes que la persona puede conceder, recortados por sus permisos, por el plan de su organización y, si lo indicas, por `resource`. Con `client_id` añade la información pública del cliente.

#### Respuesta de `GET /api/oauth/apps`

Devuelve una lista. Cada elemento tiene esta forma:

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `clientId` | `string` | Sí | Identificador público del cliente OAuth. |
| `clientName` | `string` | Sí | Nombre de la aplicación. |
| `clientUri` | `string \| null` | No | Web que declaró la aplicación al registrarse. |
| `logoUri` | `string \| null` | No | Logotipo que declaró la aplicación al registrarse. |
| `scope` | `string` | Sí | Obsoleto, se retira el 1 de febrero de 2027. Los mismos scopes que permissions, separados por espacios. Usa permissions. |
| `permissions` | `array` | Sí | Permisos que la aplicación puede ejercer con el plan actual. Cada elemento: scope, description (en castellano) e impact (destructive, write, read o identity). Primero los irreversibles. |
| `permissionCount` | `number` | Sí | Número de permisos ejercibles. Siempre igual a la longitud de permissions. |
| `unavailablePermissionCount` | `number` | Sí | Permisos concedidos que el plan actual no deja ejercer. |
| `unavailableModules` | `array` | Sí | Aplicativos no contratados de los que vienen esos permisos. Cada elemento: key y label. |
| `authorizedAt` | `string (ISO 8601)` | Sí | Fecha de la autorización vigente más antigua de esta aplicación. |
| `lastUsedAt` | `string (ISO 8601) \| null` | No | Último uso registrado entre las autorizaciones vigentes. Nulo quiere decir que no consta ninguno. |

```json
[
  {
    "clientId": "claude-desktop",
    "clientName": "Claude Desktop",
    "clientUri": "https://claude.ai",
    "logoUri": null,
    "scope": "crm:clients:view email openid profile",
    "permissions": [
      { "scope": "crm:clients:view", "description": "Ver clientes del CRM", "impact": "read" },
      { "scope": "email", "description": "Acceso a tu dirección de correo electrónico", "impact": "identity" },
      { "scope": "openid", "description": "Autenticación OpenID Connect", "impact": "identity" },
      { "scope": "profile", "description": "Acceso a tu nombre y foto de perfil", "impact": "identity" }
    ],
    "permissionCount": 4,
    "unavailablePermissionCount": 12,
    "unavailableModules": [{ "key": "marketing", "label": "Marketing" }],
    "authorizedAt": "2026-06-10T10:00:00.000Z",
    "lastUsedAt": "2026-07-27T09:12:44.000Z"
  }
]
```

Desde el 28 de julio de 2026, `scope` ya no lista todo lo que hay en el token, sino solo lo que el plan de la organización deja ejercer, con sus dos puertas: el aplicativo contratado y la capacidad activa dentro de él. Lo demás no se ha revocado: sigue concedido y se cuenta en `unavailablePermissionCount`. La fuente de verdad de lo que se puede ejecutar es siempre el servidor MCP.

## Tiempos y límites

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `Access token` | `3.600 s (1 hora)` | No | Renuévalo con el refresh token. |
| `Refresh token` | `90 días` | No | Contados desde la autorización. Rotar no alarga ese plazo. |
| `Rotación del refresh token` | `Siempre` | No | Cada renovación entrega un refresh token nuevo y anula el anterior. Reutilizar uno ya rotado revoca toda la cadena de esa autorización, salvo en el minuto siguiente a la rotación: ahí se entiende como otra parte de tu aplicación que renovaba a la vez y recibe su propio par de tokens. |
| `Código de autorización` | `60 s` | No | De un solo uso. Si se reutiliza, se revocan los tokens emitidos con él. |
| `id_token` | `3.600 s (1 hora)` | No | JWT firmado con RS256. Solo se emite si se concede el scope openid y solo en el canje del código, no en las renovaciones. |
| `Solicitud pendiente de consentimiento` | `10 minutos` | No | Si la persona tarda más en decidir, tiene que empezar de nuevo desde tu aplicación. |
| `PKCE` | `S256` | Sí | code_verifier de 43 a 128 caracteres del conjunto \[A-Za-z0-9-.\_\~]; code_challenge en Base64URL sin relleno. |
| `Autenticación del cliente` | `none, client_secret_basic, client_secret_post` | No | Los clientes del registro dinámico usan none por defecto. |

Los tokens son cadenas opacas, sin prefijo ni estructura: no intentes leerlos. Para saber si un access token sigue vivo, usa la introspección. El detalle está en [Canje, revocación e introspección de tokens](https://os.utilia.ai/docs/developers/oauth/token-revoke-introspect).

## Por dónde seguir

- [Tu primer token](https://os.utilia.ai/docs/developers/oauth/quickstart): De cero a un token funcional, paso a paso.
- [Activar OAuth en tu aplicación](https://os.utilia.ai/docs/developers/oauth/apps-panel): Credenciales, direcciones de vuelta y tipo de aplicación en el panel.
- [Flujo de autorización](https://os.utilia.ai/docs/developers/oauth/authorization-flow): Parámetros, respuestas y errores de /api/oauth/authorize.
- [Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources): Catálogo de scopes, conjuntos y qué viaja con cada servidor.
- [Iniciar sesión con UTILIA en Next.js](https://os.utilia.ai/docs/developers/oauth/sign-in-nextjs): El botón completo con Auth.js.
- [Problemas frecuentes](https://os.utilia.ai/docs/developers/oauth/troubleshooting): Errores habituales y cómo resolverlos.
