# Flujo de autorización

Cómo pedir la autorización con PKCE: parámetros de /api/oauth/authorize, qué ve la persona, cómo vuelve el código y qué errores pueden llegar.

El flujo de autorización lleva a la persona desde tu aplicación hasta UTILIA OS, donde inicia sesión y decide qué te concede, y la devuelve a tu `redirect_uri` con un código de un solo uso. Tú solo construyes la URL de `GET /api/oauth/authorize` y recoges el código; la decisión la registra la propia pantalla de consentimiento de UTILIA OS.

**Autorización con PKCE**

Participantes: Tu aplicación, Navegador y UTILIA OS.

1. Tu aplicación → Navegador: Redirige a /api/oauth/authorize. Con client_id, redirect_uri, scope, state y code_challenge.
2. Navegador → UTILIA OS: `GET /api/oauth/authorize`. UTILIA OS valida la solicitud y la guarda durante 10 minutos.
3. UTILIA OS (por su cuenta): Inicio de sesión, si hace falta, y pantalla de consentimiento.
4. UTILIA OS → Navegador (respuesta): Vuelta a tu redirect_uri con code y state. El código vale 60 segundos y sirve una sola vez.
5. Navegador → Tu aplicación: `GET /callback?code=...&state=...`. Compruebas que el state es el que enviaste.
6. Tu aplicación → UTILIA OS: `POST /api/oauth/token`. Con el code y el code_verifier.
7. UTILIA OS (por su cuenta): Comprueba que el SHA-256 del code_verifier coincide con el code_challenge.
8. UTILIA OS → Tu aplicación (respuesta): access_token, refresh_token e id_token. Access token de 1 hora; el id_token, solo si se concedió openid.

_El code_verifier nunca sale de tu aplicación hasta el canje; en la URL solo viaja su resumen, el code_challenge._

## 1. Genera el par PKCE

PKCE es obligatorio. El `code_verifier` es una cadena aleatoria que guardas en tu lado; el `code_challenge` es su SHA-256 en Base64URL sin relleno, y es lo único que viaja en la URL.

### JavaScript (navegador)

```javascript
function base64url(bytes) {
  return btoa(String.fromCharCode(...bytes))
    .replace(/\+/g, "-")
    .replace(/\//g, "_")
    .replace(/=+$/, "");
}

const codeVerifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(codeVerifier));
const codeChallenge = base64url(new Uint8Array(digest));

// Guárdalo para el canje del código
sessionStorage.setItem("code_verifier", codeVerifier);
```

### Python

```python
import base64
import hashlib
import secrets

code_verifier = secrets.token_urlsafe(32)
digest = hashlib.sha256(code_verifier.encode("ascii")).digest()
code_challenge = base64.urlsafe_b64encode(digest).rstrip(b"=").decode("ascii")
```

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `code_verifier` | `string` | Sí | De 43 a 128 caracteres del conjunto \[A-Za-z0-9-.\_\~]. Se envía al canjear el código. |
| `code_challenge` | `string` | Sí | SHA-256 del code_verifier en Base64URL sin relleno. Se envía en la URL de autorización. |
| `code_challenge_method` | `string` | Sí | Siempre S256. |

## 2. Lleva a la persona a la autorización

**`GET /api/oauth/authorize`**

Inicia la autorización y lleva a la persona a la pantalla de consentimiento de UTILIA OS

| Nombre | Tipo | Obligatorio | Descripción |
| - | - | - | - |
| `client_id` | `string` | Sí | Identificador de tu cliente. |
| `redirect_uri` | `string` | Sí | Una de las direcciones registradas para el cliente, carácter por carácter. |
| `response_type` | `string` | Sí | Siempre code. |
| `code_challenge` | `string` | Sí | Base64URL del SHA-256 del code_verifier, de 43 a 128 caracteres. |
| `code_challenge_method` | `string` | Sí | Siempre S256. |
| `scope` | `string` | No | Scopes separados por espacios. Si lo omites, se usan los allowedScopes del registro del cliente o, si no tiene, solo openid, profile y email. |
| `state` | `string` | No | Valor aleatorio contra la falsificación de peticiones. Vuelve tal cual en la redirección; compruébalo siempre. |
| `resource` | `string` | No | Servidor MCP al que irá atado el token (RFC 8707), por ejemplo <https://os.utilia.ai/api/mcp/crm>. Recorta los scopes al dominio de ese servidor. Sin él, y sin recurso por defecto en el registro del cliente, solo se conceden openid, profile y email. |
| `nonce` | `string` | No | Valor aleatorio que vuelve dentro del id_token. Verifícalo para evitar reutilizaciones. |
| `prompt` | `string` | No | none, login, select_account o consent. Ver más abajo. |
| `login_hint` | `string` | No | Correo con el que rellenar el inicio de sesión. Hasta 255 caracteres del conjunto \[A-Za-z0-9.\_@+-]. |
| `theme` | `string` | No | light o dark. Se acepta y se valida, pero hoy la pantalla de consentimiento muestra el tema que tenga elegido la persona. |

Un valor no admitido en `response_type`, `code_challenge_method`, `prompt`, `theme` o `login_hint`, o un `code_challenge` mal formado, hace que la solicitud se rechace sin volver a tu aplicación.

### JavaScript

```javascript
const params = new URLSearchParams({
  client_id: "TU_CLIENT_ID",
  redirect_uri: "https://mi-app.com/callback",
  response_type: "code",
  scope: "openid profile email crm:clients:view",
  resource: "https://os.utilia.ai/api/mcp/crm",
  code_challenge: codeChallenge,
  code_challenge_method: "S256",
  state: crypto.randomUUID(),
});

window.location.href = `https://os.utilia.ai/api/oauth/authorize?${params}`;
```

### Python

```python
import secrets
from urllib.parse import urlencode

params = urlencode({
    "client_id": "TU_CLIENT_ID",
    "redirect_uri": "https://mi-app.com/callback",
    "response_type": "code",
    "scope": "openid profile email crm:clients:view",
    "resource": "https://os.utilia.ai/api/mcp/crm",
    "code_challenge": code_challenge,
    "code_challenge_method": "S256",
    "state": secrets.token_urlsafe(16),
})

auth_url = f"https://os.utilia.ai/api/oauth/authorize?{params}"
```

UTILIA OS guarda la solicitud durante **10 minutos** y lleva a la persona a su pantalla de consentimiento. Si no ha iniciado sesión, primero pasa por el inicio de sesión y después vuelve sola. Allí ve qué pides, agrupado en permisos comprensibles, y puede desactivar los que no quiera conceder. Lo cuenta [Pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen).

## 3. Recoge la respuesta

Si la persona autoriza, vuelve a tu `redirect_uri` con el código:

```
https://mi-app.com/callback?code=CODIGO&state=TU_STATE&iss=https%3A%2F%2Fos.utilia.ai
```

`iss` es el emisor (RFC 9207) y llega en todas las respuestas, también en las de error. Si tu aplicación habla con más de un servidor de autorización, comprueba que es `https://os.utilia.ai` antes de canjear el código.

El código vale **60 segundos** y sirve **una vez**; si se reutiliza, se revocan los tokens emitidos con él. Canjéalo enseguida en `POST /api/oauth/token`, como explica [Canje, revocación e introspección de tokens](https://os.utilia.ai/docs/developers/oauth/token-revoke-introspect).

Si la persona pulsa **No permitir**, vuelve con un error:

```
https://mi-app.com/callback?error=access_denied&error_description=The+user+denied+the+authorization+request&state=TU_STATE&iss=https%3A%2F%2Fos.utilia.ai
```

> **Atención: El token puede llevar menos de lo que pediste**
>
> La persona puede desactivar grupos de permisos, y además cada scope se recorta por sus permisos y por el plan de su organización. Lee siempre el campo `scope` de la respuesta del token y no des por hecho el conjunto que pediste.

## Errores

Dónde aparece el error depende de lo que falla. Si tu `client_id` o tu `redirect_uri` no son válidos, UTILIA OS no puede devolver a la persona a tu aplicación sin riesgo y le muestra su propia página de error.

**¿Dónde llega un error de autorización?**

¿El cliente existe y está activo, y la redirect_uri está registrada?

- No: **Página de error de UTILIA OS** (`invalid_client · invalid_redirect_uri`). La persona no vuelve a tu aplicación.
- Sí: ¿Qué falla?
  - Scope o resource: **Tu redirect_uri** (`invalid_scope · invalid_request`). Un scope que no existe o no está permitido para el cliente, o un resource no permitido.
  - prompt=none: **Tu redirect_uri** (`login_required`). No hay sesión utilizable (ver más abajo).
  - La persona no autoriza: **Tu redirect_uri** (`access_denied`). Pulsó No permitir, o Volver en «Acceso no autorizado».
  - Nada que conceder: **Página de error de UTILIA OS** (`invalid_scope`). Al decidir, le faltan permisos o su organización no tiene contratado el aplicativo.

En todos los casos que vuelven a tu `redirect_uri`, llegan `error`, `error_description` y tu `state`.

Si a la persona no le queda ningún permiso que conceder, la pantalla le dice «Acceso no autorizado» y el botón **Volver** te la devuelve con `access_denied`.

## Parámetros de OpenID Connect

### `prompt`

| Valor | Qué hace |
| - | - |
| `none` | Exige una sesión ya abierta. El navegador no envía la sesión de UTILIA OS al servidor de autorización, así que en una redirección normal la respuesta es siempre `login_required`: no sirve para renovar en silencio. Para eso, usa el refresh token. |
| `login` | Obliga a la persona a iniciar sesión otra vez, aunque ya la tuviera abierta. |
| `select_account` | Si el navegador tiene varias cuentas guardadas, la pantalla muestra un selector para cambiar de cuenta; si no tiene ninguna, lleva al inicio de sesión para añadir una. |
| `consent` | Obliga a mostrar la pantalla de consentimiento, sin selector de cuenta. Es el único valor que la muestra también a las aplicaciones propias de UTILIA OS. |

Las aplicaciones propias de UTILIA OS se saltan la pantalla de consentimiento cuando la persona ya tiene sesión. Esa marca solo la pone UTILIA OS: una aplicación de terceros pasa siempre por la pantalla.

### `login_hint`

Rellena el correo en el inicio de sesión. Solo admite letras, dígitos y los símbolos `. _ @ + -`, con un máximo de 255 caracteres. Cualquier otro carácter hace que la solicitud se rechace.

### `max_age`

Se acepta, pero no tiene efecto: UTILIA OS no registra el momento del último inicio de sesión y no puede evaluarlo. Si necesitas una autenticación reciente, por ejemplo antes de una acción delicada, usa `prompt=login`.

### `nonce` y `resource`

- `nonce` vuelve tal cual dentro del `id_token`. Genéralo aleatorio en cada petición y compruébalo al recibir el token.
- `resource` fija el destinatario del token y recorta los scopes al dominio de ese servidor MCP. Si lo omites y el cliente tiene un recurso por defecto en su registro, se usa ese. Sin ninguno de los dos, la autorización solo concede `openid`, `profile` y `email`: si pides scopes de un servidor MCP, responde `invalid_request` con las direcciones de las áreas. La tabla de qué viaja con cada servidor está en [Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources#filtrado-por-recurso).
- La dirección única `https://os.utilia.ai/api/mcp`, con todas las áreas, se retiró el 20 de octubre de 2026. Pedirla como `resource` devuelve `invalid_request`: usa la dirección del área, por ejemplo `https://os.utilia.ai/api/mcp/crm`.

## Siguiente paso

- [Canje, revocación e introspección](https://os.utilia.ai/docs/developers/oauth/token-revoke-introspect): Cambia el código por tokens y gestiona su ciclo de vida.
- [Pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen): Qué ve la persona y qué puede desactivar.
- [Scopes y claims de OpenID Connect](https://os.utilia.ai/docs/developers/oauth/oidc-scopes-and-claims): Qué datos de la persona recibes en el id_token.
- [Problemas frecuentes](https://os.utilia.ai/docs/developers/oauth/troubleshooting): Errores habituales y cómo resolverlos.
