OAuth con el SDK
Usa el SDK en nombre de una persona con OAuth 2.1 y PKCE, guarda sus tokens donde quieras y deja que el SDK los renueve.
Actualizado el . 5 min de lectura
Incluido en todos los planes. Ver precios
Con OAuth tu aplicación actúa en nombre de una persona de UTILIA OS y solo con los permisos que ella acepta, en lugar de usar la clave de la aplicación. Es la vía para un botón «Iniciar sesión con UTILIA» o para una herramienta que trabaja con los datos de quien la usa. El SDK se ocupa de PKCE, del intercambio del código por tokens y de renovarlos antes de que caduquen.
Si lo que quieres es solo el botón de inicio de sesión, tienes la guía paso a paso en Iniciar sesión con UTILIA usando el SDK JavaScript y en su equivalente para Python. Los conceptos (scopes, consentimiento, rotación de tokens) están en OAuth: visión general.
Configurar el cliente con OAuth
En lugar de la clave, pasa la configuración oauth. Los campos obligatorios son el identificador de cliente y la dirección de vuelta registrada en tu aplicación.
| JavaScript | Python | Obligatorio | Qué es |
|---|---|---|---|
clientId | client_id | Sí | Identificador de tu aplicación OAuth. |
redirectUri | redirect_uri | Sí | Dirección de vuelta, exactamente la registrada. |
clientSecret | client_secret | No | Secreto, solo para aplicaciones confidenciales (de servidor). |
scopes | scopes | No | Scopes que pides. Por defecto openid, profile y email. |
tokenStorage | token_storage | No | Dónde se guardan los tokens. Por defecto, en memoria. |
import { UtiliaSDK, FileTokenStorage } from '@utilia-os/sdk-js';
const sdk = new UtiliaSDK({
baseURL: 'https://os.utilia.ai/api',
oauth: {
clientId: process.env.UTILIA_CLIENT_ID!,
clientSecret: process.env.UTILIA_CLIENT_SECRET, // solo aplicaciones confidenciales
redirectUri: 'http://localhost:8080/callback',
scopes: ['openid', 'profile', 'email'],
tokenStorage: new FileTokenStorage('.utilia-tokens.json'),
},
});PKCE va incluido: el SDK genera el verificador, calcula el desafío con SHA-256 y lo envía en cada paso. No tienes que programarlo.
El flujo de autorización
El recorrido completo, con lo que hace el SDK en cada paso:
Autorización con PKCE desde el SDK
Participan: Tu aplicación, Navegador, UTILIA OS.
Nota en Tu aplicación:
getAuthorizationUrl() genera el verificador, el desafío S256 y el state, y guarda el estado pendiente en el almacenamiento de tokens.De Tu aplicación a Navegador:
Redirige a la dirección de autorización
Lleva client_id, redirect_uri, scope, state y code_challenge.De Navegador a UTILIA OS:
GET /api/oauth/authorize
Nota en UTILIA OS:
La persona inicia sesión y acepta los permisos en la pantalla de consentimiento.De UTILIA OS a Navegador:
Vuelve a redirectUri con code y state (respuesta)
De Navegador a Tu aplicación:
Llega a tu ruta de vuelta
Pasas el code y el state a handleCallback(); el SDK comprueba que el state coincide.De Tu aplicación a UTILIA OS:
POST /api/oauth/token
Con el code y el code_verifier guardado.De UTILIA OS a Tu aplicación:
access_token y refresh_token (respuesta)
El SDK los guarda; desde aquí cada petición lleva Authorization: Bearer y el token se renueva solo.
// Antes de redirigir (con tema opcional para el consentimiento)
const authUrl = await sdk.oauth.getAuthorizationUrl({ theme: 'dark' });
// En tu ruta de vuelta
const code = url.searchParams.get('code')!;
const state = url.searchParams.get('state') ?? undefined;
await sdk.oauth.handleCallback(code, undefined, state);
const user = await sdk.oauth.getUserInfo();
console.log(`Hola, ${user.name}`);La comprobación del state solo se hace si se lo pasas a handleCallback(): pásalo siempre. Las dos funciones de la dirección aceptan además scopes, state, prompt, loginHint (login_hint), nonce, resource y maxAge (max_age), y theme con light o dark para fijar el tema de la pantalla de consentimiento.
Si prefieres gestionar tú el verificador y el state, getLoginUrl() (get_login_url()) devuelve la dirección junto con los dos valores, y después se los pasas a handleCallback().
En el navegador, el almacenamiento en memoria no sobrevive a la redirección
El almacenamiento por defecto guarda el estado PKCE en memoria. Si tu aplicación web cambia de página para ir a autorizar, al volver ese estado ya no existe y handleCallback() falla con «No se encontró el estado PKCE pendiente». Usa un tokenStorage propio que implemente también getPendingState, setPendingState y clearPendingState (por ejemplo, sobre sessionStorage), o la ventana emergente que se explica más abajo.
Dónde se guardan los tokens
| Almacenamiento | Dónde guarda | Para qué |
|---|---|---|
MemoryTokenStorage | En memoria del proceso | Por defecto. Se pierde al reiniciar. |
FileTokenStorage(ruta) | Un fichero JSON (y otro aparte para el estado PKCE), con permisos 600 | Scripts y herramientas de terminal que deben recordar la sesión entre ejecuciones. |
| Uno propio | Donde tú decidas (base de datos, Redis…) | Servidores y aplicaciones web. |
El fichero de tokens contiene credenciales: añádelo a .gitignore.
Para uno propio, en JavaScript implementa la interfaz TokenStorage (getTokens, setTokens, clearTokens y, opcionalmente, los tres del estado PKCE). En Python hereda de la clase abstracta TokenStorage y define get_tokens, set_tokens y clear_tokens (asíncronos); para UtiliaSDKSync, de SyncTokenStorage, con get_tokens_sync, set_tokens_sync y clear_tokens_sync. En Python, OAuthTokens es un dataclass: se serializa con dataclasses.asdict().
import type { OAuthTokens, TokenStorage } from '@utilia-os/sdk-js';
class RedisTokenStorage implements TokenStorage {
constructor(private readonly key: string) {}
async getTokens(): Promise<OAuthTokens | null> {
const data = await redis.get(this.key);
return data ? JSON.parse(data) : null;
}
async setTokens(tokens: OAuthTokens): Promise<void> {
await redis.set(this.key, JSON.stringify(tokens));
}
async clearTokens(): Promise<void> {
await redis.del(this.key);
}
}Un cliente con OAuth guarda una sola sesión: el almacenamiento no distingue entre personas. En un servidor que atiende a varias, crea un cliente por persona o un almacenamiento con una clave por sesión, como en el ejemplo.
Renovación y caducidad
Antes de cada petición, si al token de acceso le quedan menos de 30 segundos, el SDK lo renueva con el token de renovación. Si aun así el servidor responde 401, lo renueva y repite la petición una vez (en Python, solo cuando retry_attempts es mayor que 1). Las renovaciones simultáneas se agrupan en una sola, porque el servidor rota el token de renovación y dos usos del mismo invalidarían la sesión.
Cuando ya no se puede renovar, la petición falla con UNAUTHORIZED: es el momento de pedir a la persona que vuelva a autorizar.
import { ErrorCode, UtiliaSDKError } from '@utilia-os/sdk-js';
try {
await sdk.tickets.list('user-456');
} catch (error) {
if (error instanceof UtiliaSDKError && error.code === ErrorCode.UNAUTHORIZED) {
window.location.href = await sdk.oauth.getAuthorizationUrl();
}
}ErrorCode incluye el valor AUTH_EXPIRED, pero hoy el SDK no lo emite nunca: una sesión caducada llega siempre como UNAUTHORIZED. No bases la lógica de volver a autorizar en AUTH_EXPIRED.
Otras operaciones de la sesión
| JavaScript | Python | Qué hace |
|---|---|---|
isAuthenticated() | is_authenticated() | true si hay un token vigente o uno de renovación. |
getUserInfo() | get_user_info() | Datos de la persona (sub, name, email…). |
getAccessToken() | get_access_token() | Token de acceso vigente, renovado si hacía falta. |
refreshToken() | refresh_token() | Fuerza la renovación. |
logout() | logout() | Revoca el token de acceso y borra el almacenamiento. |
revokeToken(tipo?) | revoke_token(tipo) | Revoca el token de acceso o el de renovación y borra el almacenamiento. |
revoke(token, pista?) | revoke(token, token_type_hint) | Revoca un token cualquiera sin tocar el almacenamiento. |
introspect(token, pista?) | introspect(token, token_type_hint) | Consulta el estado de un token emitido a tu aplicación. Solo para aplicaciones confidenciales: sin clientSecret falla antes de llamar al servidor. |
getAuthorizedApps() | get_authorized_apps() | Aplicaciones que la persona ha autorizado, con permisos ya recortados por el plan. |
revokeApp(clientId) | revoke_app(client_id) | Retira la autorización de una aplicación e invalida sus tokens. |
Ventana emergente (solo JavaScript en el navegador)
loginWithPopup() abre la autorización en una ventana emergente y devuelve los tokens sin que tu página cambie. Acepta las mismas opciones que la dirección de autorización más width, height y timeout (por defecto 500 × 700 píxeles y 5 minutos).
Para que funcione, la página de tu redirectUri tiene que avisar a la ventana que la abrió, desde el mismo origen, con window.opener.postMessage({ type: 'oauth-mcp-success', code, state }, origen) (o type: 'oauth-mcp-error' con error si la persona rechaza). Si el navegador bloquea la ventana, la persona la cierra o se agota el tiempo, la promesa falla con un error explicativo.