# Tu primer token en cinco minutos

Registra un cliente, genera PKCE, autoriza y canjea el código: de cero a un access token de UTILIA OS con curl, JavaScript o Python.

Esta guía te lleva de cero a un access token funcional sin salir de la página. Solo necesitas una cuenta de UTILIA OS con la que autorizar y curl o cualquier cliente HTTP. Al final comprobarás el token pidiendo los datos de la persona a `GET /api/oauth/userinfo`. Antes de empezar, retén estos tres plazos: marcan cuánto tiempo tienes en cada paso.

**Tres plazos que vas a usar**

- Código de autorización: 60 segundos. Sirve una sola vez: canjéalo nada más recibirlo.
- Access token: 1 hora. Después, renuévalo con el refresh token.
- Refresh token: 90 días. Desde la autorización. Cambia en cada renovación.

El orden completo de la conversación entre tu aplicación, el navegador y UTILIA OS está dibujado en [Flujo de autorización](https://os.utilia.ai/docs/developers/oauth/authorization-flow).

1. **Registra tu aplicación**

   Envía una petición al endpoint de registro dinámico. Es público y no necesita autenticación.

   **`POST /api/oauth/register`**

   Registro dinámico de clientes OAuth (público)

   **curl**

   ```bash
   curl -X POST https://os.utilia.ai/api/oauth/register \
     -H "Content-Type: application/json" \
     -d '{
       "client_name": "Agenda Nubeta (pruebas)",
       "redirect_uris": ["http://localhost:3000/callback"]
     }'
   ```

   **JavaScript**

   ```javascript
   const response = await fetch("https://os.utilia.ai/api/oauth/register", {
     method: "POST",
     headers: { "Content-Type": "application/json" },
     body: JSON.stringify({
       client_name: "Agenda Nubeta (pruebas)",
       redirect_uris: ["http://localhost:3000/callback"],
     }),
   });

   const client = await response.json();
   console.log(client.client_id);
   ```

   **Python**

   ```python
   import requests

   response = requests.post("https://os.utilia.ai/api/oauth/register", json={
       "client_name": "Agenda Nubeta (pruebas)",
       "redirect_uris": ["http://localhost:3000/callback"],
   })

   client = response.json()
   print(client["client_id"])
   ```

   Respuesta:

   ```json
   {
     "client_id": "mcp_3f9a1c2b7d4e5f60718293a4b5c6d7e8",
     "client_id_issued_at": 1743897600,
     "redirect_uris": ["http://localhost:3000/callback"],
     "grant_types": ["authorization_code", "refresh_token"],
     "response_types": ["code"],
     "token_endpoint_auth_method": "none",
     "client_name": "Agenda Nubeta (pruebas)"
   }
   ```

   Con `token_endpoint_auth_method: "none"` el cliente es público y no tiene secreto: PKCE lo protege. Las direcciones de vuelta deben usar `https`, salvo `localhost`, `127.0.0.1` y `[::1]`, que admiten `http`. Si prefieres crear la aplicación desde la interfaz, con clave secreta y enlaces legales, sigue [Activar OAuth en tu aplicación](https://os.utilia.ai/docs/developers/oauth/apps-panel).

   Tres cosas del registro dinámico que conviene saber antes de seguir:

   - **Es público y tiene límite.** No pide cuenta y admite 30 registros por hora desde una misma dirección IP.
   - **Pon el nombre de tu aplicación.** Un cliente público con el mismo nombre y las mismas direcciones de vuelta que otro ya registrado devuelve ese mismo `client_id`: si copias el ejemplo tal cual, compartirías cliente con quien haya hecho lo mismo.
   - **Cualquier persona puede autorizarlo.** El cliente no pertenece a ninguna organización: lo autoriza cualquiera con cuenta en UTILIA OS, sobre los datos de su propia organización. Si tu aplicación es solo para tu equipo, lee [Quién puede autorizar tu aplicación](https://os.utilia.ai/docs/developers/oauth/apps-panel#quién-puede-autorizar-tu-aplicación).
2. **Genera el par PKCE**

   Crea un `code_verifier` aleatorio y deriva de él el `code_challenge` con SHA-256 en Base64URL sin relleno.

   **Bash**

   ```bash
   CODE_VERIFIER=$(openssl rand -base64 32 | tr -d '=' | tr '/+' '_-')
   CODE_CHALLENGE=$(printf '%s' "$CODE_VERIFIER" | openssl dgst -sha256 -binary | openssl base64 | tr -d '=' | tr '/+' '_-')

   echo "code_verifier:  $CODE_VERIFIER"
   echo "code_challenge: $CODE_CHALLENGE"
   ```

   **JavaScript**

   ```javascript
   import { randomBytes, createHash } from "node:crypto";

   const codeVerifier = randomBytes(32).toString("base64url");
   const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");

   console.log("code_verifier: ", codeVerifier);
   console.log("code_challenge:", codeChallenge);
   ```

   **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")

   print("code_verifier: ", code_verifier)
   print("code_challenge:", code_challenge)
   ```

   Guarda el `code_verifier`: lo necesitarás en el paso 5. Nunca viaja en la URL de autorización.
3. **Abre la URL de autorización**

   Monta la URL con estos parámetros y ábrela en el navegador. Los espacios de `scope` van codificados como `%20`.

   ```
   https://os.utilia.ai/api/oauth/authorize?client_id=CLIENT_ID&redirect_uri=http://localhost:3000/callback&response_type=code&code_challenge=CODE_CHALLENGE&code_challenge_method=S256&scope=openid%20profile%20email&state=VALOR_ALEATORIO
   ```

   | Nombre | Tipo | Obligatorio | Descripción |
   | - | - | - | - |
   | `client_id` | `string` | Sí | El identificador del paso 1. |
   | `redirect_uri` | `string` | Sí | Una de las direcciones registradas, exactamente igual. |
   | `response_type` | `string` | Sí | Siempre code. |
   | `code_challenge` | `string` | Sí | El challenge del paso 2. |
   | `code_challenge_method` | `string` | Sí | Siempre S256. No se admite plain. |
   | `scope` | `string` | No | Scopes separados por espacios. Si lo omites, se piden solo openid, profile y email. |
   | `state` | `string` | No | Valor aleatorio contra la falsificación de peticiones. Vuelve tal cual en la redirección. |

   UTILIA OS pide a la persona que inicie sesión, si no lo ha hecho, y le muestra la [pantalla de consentimiento](https://os.utilia.ai/docs/developers/oauth/consent-screen).
4. **Recoge el código**

   Cuando la persona autoriza, UTILIA OS la devuelve a tu `redirect_uri` con el código:

   ```
   http://localhost:3000/callback?code=abc123def456&state=VALOR_ALEATORIO
   ```

   Comprueba que `state` coincide con el que enviaste y quédate con `code`. Tienes 60 segundos para canjearlo.
5. **Canjea el código por tokens**

   **`POST /api/oauth/token`**

   Canje del código por tokens

   El endpoint admite `application/x-www-form-urlencoded` (lo habitual en OAuth) y JSON.

   **curl**

   ```bash
   curl -X POST https://os.utilia.ai/api/oauth/token \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d 'grant_type=authorization_code' \
     -d 'code=AUTH_CODE' \
     -d 'redirect_uri=http://localhost:3000/callback' \
     -d 'code_verifier=CODE_VERIFIER' \
     -d 'client_id=CLIENT_ID'
   ```

   **JavaScript**

   ```javascript
   const response = await fetch("https://os.utilia.ai/api/oauth/token", {
     method: "POST",
     headers: { "Content-Type": "application/x-www-form-urlencoded" },
     body: new URLSearchParams({
       grant_type: "authorization_code",
       code: "AUTH_CODE",
       redirect_uri: "http://localhost:3000/callback",
       code_verifier: "CODE_VERIFIER",
       client_id: "CLIENT_ID",
     }),
   });

   const tokens = await response.json();
   console.log(tokens.access_token);
   ```

   **Python**

   ```python
   import requests

   response = requests.post("https://os.utilia.ai/api/oauth/token", data={
       "grant_type": "authorization_code",
       "code": "AUTH_CODE",
       "redirect_uri": "http://localhost:3000/callback",
       "code_verifier": "CODE_VERIFIER",
       "client_id": "CLIENT_ID",
   })

   tokens = response.json()
   print(tokens["access_token"])
   ```

   Respuesta:

   ```json
   {
     "access_token": "TU_ACCESS_TOKEN",
     "token_type": "Bearer",
     "expires_in": 3600,
     "refresh_token": "TU_REFRESH_TOKEN",
     "scope": "openid profile email",
     "id_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im9pZGMtcHJvZC0xIn0..."
   }
   ```

   Cada vez que renuevas, recibes un refresh token nuevo y el anterior deja de valer: guarda siempre el último. Lo explica [Rotación del refresh token](https://os.utilia.ai/docs/developers/oauth/refresh-token-rotation).
6. **Usa el token**

   Pide los datos de la persona:

   **`GET /api/oauth/userinfo`**

   Datos de la persona autenticada (access token con openid)

   **curl**

   ```bash
   curl -H "Authorization: Bearer ACCESS_TOKEN" \
     https://os.utilia.ai/api/oauth/userinfo
   ```

   **JavaScript**

   ```javascript
   const response = await fetch("https://os.utilia.ai/api/oauth/userinfo", {
     headers: { Authorization: "Bearer ACCESS_TOKEN" },
   });

   console.log(await response.json());
   ```

   **Python**

   ```python
   import requests

   response = requests.get(
       "https://os.utilia.ai/api/oauth/userinfo",
       headers={"Authorization": "Bearer ACCESS_TOKEN"},
   )
   print(response.json())
   ```

   Respuesta:

   ```json
   {
     "sub": "3b1f6c2e-8a4d-4f0b-9c7e-2d5a1e6f8b90",
     "name": "María García",
     "given_name": "María",
     "family_name": "García",
     "email": "maria@ejemplo.example",
     "email_verified": true
   }
   ```

   Si la persona tiene foto de perfil, también llega `picture`. Para trabajar con sus datos, pide en el paso 3 los scopes de un servidor MCP junto con su `resource` y llama a ese servidor con el token. Lo tienes en [Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources).

## Si algo falla

- **`invalid_grant`**: el código ha caducado, ya se usó o el `code_verifier` no corresponde al `code_challenge`. Genera un par PKCE nuevo y vuelve al paso 3.
- **`invalid_grant` con `redirect_uri mismatch`**: la dirección del canje no es idéntica a la de la autorización. `http://localhost:3000/callback` y `http://localhost:3000/callback/` son distintas.
- **`invalid_request` con `code_verifier is required for PKCE`**: falta el `code_verifier` en el canje.

El resto de casos está en [Problemas frecuentes con OAuth](https://os.utilia.ai/docs/developers/oauth/troubleshooting).

## Siguientes pasos

- [Activar OAuth en tu aplicación](https://os.utilia.ai/docs/developers/oauth/apps-panel): Crea la aplicación en el panel con clave secreta y enlaces legales.
- [Scopes y recursos](https://os.utilia.ai/docs/developers/oauth/scopes-and-resources): Qué scopes pedir y cómo atar el token a un servidor MCP.
- [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.
- [Botones y marca](https://os.utilia.ai/docs/developers/oauth/buttons-and-branding): El botón oficial y los recursos de marca.
