Agentes en nombre del usuario: OAuth, consentimiento y tokens que no viven en el prompt
Resumen
Un agente que lee el calendario o manda un correo no es el usuario: es un cliente OAuth. Authorization code + PKCE, scopes mínimos, consentimiento visible y tokens fuera del contexto. Contrato para MCP y plugins, distinto de HITL y de secretos de servicio.

Qué resuelve
Esta pieza se queda en la decisión práctica: qué instalar, qué riesgo agrega y cómo aplicarlo sin romper operación.
Un agente que “lee tu Gmail” no es tú. Es un cliente OAuth que pide permiso al dueño del recurso y, si el dueño consiente, recibe un access token con scopes concretos. Si ese token acaba en el system prompt, en un log o en el historial del modelo, el consentimiento se rompe: cualquiera con el contexto actúa en nombre del usuario sin pasar otra vez por el authorization server.
No es human-in-the-loop. HITL pausa una acción para que un humano la apruebe; OAuth autoriza quién puede llamar a la API y con qué scopes. No es secretos de agentes: un TELEGRAM_BOT_TOKEN es credencial de servicio; el token del usuario es delegación y caduca. Tampoco es PII: redactar un DNI no sustituye auditar aud y scope en cada request.
Contrato: el usuario consiente en el navegador; el agente usa el token; el modelo nunca lo ve.
Tres identidades, no una
OAuth 2.0 (RFC 6749) y el BCP de 2025 (RFC 9700) separan roles. En un agente que actúa en nombre de alguien, los tres existen a la vez:
| Rol | Quién es en un agente | Qué no es |
|---|---|---|
| Resource owner | El usuario que posee el calendario, el repo o el buzón | El modelo |
| Client | Tu runtime / MCP client / plugin host | Un “usuario extra” |
| Resource server | Gmail, GitHub, el MCP server HTTP | El LLM |
La spec MCP (2025-06-18) lo dice explícito: el MCP server HTTP es un resource server OAuth 2.1; el MCP client hace requests on behalf of the resource owner. STDIO no usa este flujo: las credenciales salen del entorno, no de un redirect. Si mezclas las tres identidades, terminas con password grant o un bot que manda correo con la sesión de quien lo desplegó.
El único grant que un agente público puede usar
RFC 9700, sección 2.1.1: los clientes públicos MUST usar PKCE (RFC 7636) para el authorization code grant. El authorization server MUST soportar PKCE y MUST publicar code_challenge_methods_supported. El método que no expone el verifier es S256.
RFC 9700 §2.4: el resource owner password credentials grant MUST NOT usarse. Expone la contraseña del dueño al cliente. Un agente que pide “pégame tu password de Google” viola el BCP. Implicit grant tampoco: el fragmento del browser ya no sostiene el modelo. Client credentials es para el servicio, no para actuar como Ana.
Flujo mínimo:
- El runtime genera
code_verifierycode_challenge(S256). - Abre el navegador del usuario en el authorization endpoint con
code_challenge,state,scopey —en MCP—resource(RFC 8707). - El usuario autentica y consiente scopes visibles.
- El callback recibe un code, no un token.
- El runtime canjea code +
code_verifieren el token endpoint. El modelo no participa.
OpenAI, como host de plugins MCP, exige lo mismo: si el metadata del AS omite S256, el conector no es soportado.
Consentimiento que el usuario puede leer
Un scope https://mail.google.com/ no es consentimiento. Es un string opaco. El contrato de producto:
- Lista acciones, no APIs: “leer eventos de esta semana”, no
calendar.readonlycrudo si puedes evitarlo. - Incremental: pide
calendar.readonlyhoy;calendar.eventsmañana, con otra pantalla. OpenAI reautoriza conid_token_hintprecisamente para no repetir el login entero al pedir scopes extra. - Default-deny en write: enviar correo, crear issue, borrar archivo → segundo factor de HITL aunque el token ya exista.
- Revocación: URL de “desconectar” que llama al revocation endpoint y borra refresh tokens. Sin eso, el consentimiento es de un solo sentido.
MCP añade scopes_supported en el Protected Resource Metadata (RFC 9728). El host (ChatGPT, Codex, tu CLI) puede explicar esos scopes antes de abrir el browser. Si dejas el array vacío, el usuario ve “este agente quiere acceder a tus datos” y el consentimiento es teatro.

Tokens: fuera del contexto, atados al recurso
MCP y OpenAI coinciden en el manejo:
- El access token viaja en el header
Authorization(esquema Bearer), nunca en query string. - Cada request HTTP lo lleva, aunque sea la misma sesión lógica.
- El parámetro
resource(RFC 8707) MUST ir en authorize y en token; identifica el MCP server canónico (https://mcp.example.com, sin fragmento). - El resource server MUST validar que el token fue emitido para él (
aud). Token passthrough —reenviar el token de ChatGPT a otro API— está prohibido en las security best practices de MCP. - Access tokens cortos; refresh tokens de clientes públicos MUST rotar (OAuth 2.1 §4.3.1, citado por MCP).
- 401 si el token es inválido o expiró; 403 si el scope no alcanza. El 401 incluye
WWW-Authenticateconresource_metadatapara redescubrir el AS.
Dónde se guarda: bóveda del runtime, keychain, cookie httpOnly del BFF. Cero system prompt, cero tool result, cero traza que el modelo pueda citar. Eso es la misma higiene que secretos, con una diferencia: el secreto de servicio es tuyo; el token del usuario es suyo y se revoca cuando el usuario lo dice.
Descubrimiento (MCP): 401 → /.well-known/oauth-protected-resource → authorization_servers → /.well-known/oauth-authorization-server. El cliente no hardcodea el token endpoint.
Matriz: qué grant para qué agente
| Situación | Grant / patrón | Por qué |
|---|---|---|
| Plugin ChatGPT / Codex llama a tu MCP con datos del usuario | Auth code + PKCE S256 + resource | OpenAI actúa como client; el usuario consiente |
| CLI local abre el browser una vez | Auth code + PKCE; loopback http://127.0.0.1:<port>/callback | RFC 9700 permite puerto variable solo en localhost |
| Cron que indexa tu Drive de empresa | Client credentials o cuenta de servicio | No hay resource owner interactivo |
| “Pégame el refresh token en el chat” | Prohibido | El modelo se convierte en authorization server falso |
| Password grant “para no abrir browser” | MUST NOT (RFC 9700 §2.4) | Filtra la contraseña al agente |
Redirect URI: matching exacto (RFC 9700 §2.1), salvo el puerto de localhost. Open redirectors están prohibidos. Mix-up: OpenAI exige iss (RFC 9207) igual al issuer del metadata; si el AS anuncia soporte y lo omite, el host rechaza el callback.
Anti-patrones que rompen el consentimiento
- Token en el prompt. El modelo lo recita, lo manda a otro tool o lo deja en el transcript. Trata el token como secreto, no como “contexto útil”.
- Un token para todos los tenants. El
subdel dueño tiene que viajar con cada llamada al resource server. Si no, el agente de Ana opera el Drive de Luis. - Scopes
*“por si acaso”. Consentimiento inflado. Si mañana el modelo alucina undelete, el token ya lo permite. - Passthrough. Recibir un Bearer de ChatGPT y reenviarlo a GitHub. MCP lo prohíbe: cada RS valida su audiencia.
- STDIO + OAuth redirect. La spec MCP dice que STDIO SHOULD NOT seguir este capítulo: las credenciales salen del entorno. No abras un browser desde un subprocess sin TTY y llames a eso “consentimiento”.
- Log del
Authorizationheader. Misma clase de fuga que volcar PII. Redacta el header entero.
La capa MCP de permisos (tool poisoning) sigue aplicando: un tool description malicioso no debe poder ampliar scopes. OAuth limita qué API; el allowlist de tools limita qué llamada.

Checklist de release
- Grant = authorization code + PKCE S256; password e implicit ausentes.
- AS publica
code_challenge_methods_supported: ["S256"]y lo exige si llegócode_challenge. - Redirect URIs con matching exacto; localhost solo para CLI.
- Scopes mínimos, textos de consentimiento en el idioma del usuario, write con HITL extra.
- Token en header;
resource/audvalidados; 401 conWWW-Authenticate. - Refresh rotado en clientes públicos; revocación expuesta al usuario.
- Cero token en prompts, tool results, evals o logs.
- MCP HTTP: Protected Resource Metadata + AS metadata; STDIO no finge OAuth.
FAQ
¿El agente puede guardar el refresh token? Sí, en la bóveda del runtime, cifrado, por user_id. No en sqlite junto al historial del chat. Rotación obligatoria en clientes públicos.
¿Y si el proveedor no soporta PKCE? No conectes un agente público. RFC 9700 exige PKCE en el AS. OpenAI rechaza el conector si falta S256.
¿Client credentials “en nombre del usuario”? No. Eso es el servicio hablando. Para actuar como Ana hace falta su consentimiento (o un on-behalf-of de un AS que ya autenticó a Ana).
¿HITL sustituye OAuth? No. HITL aprueba un efecto (“¿mando este correo?”). OAuth aprueba el poder de mandarlo. Necesitas los dos en writes.
Siguiente lectura: el hub de seguridad, coste y operación y el curso de instalar un agente.
Lecturas relacionadas
Sigue explorando AgentOps y otras piezas para builders.

Cache semántico en agentes LLM: exact-match primero, umbral alto, clave con tenant

LLM-as-judge para agentes: rúbrica, schema y calibración (el juez no es la verdad)

Circuit breaker y kill switch en agentes: cortar la dependencia, no el proceso
