Authorization

OAuth 2.0

OAuth 2.0 es un framework para la autorización delegada, no para la autenticación. Permite que un usuario conceda a una aplicación acceso limitado a sus datos sin compartir su contraseña.

intermediate16 min readUpdated 16 sept 2026
authorize.ts
ts
// authorize.ts
import { randomBytes, createHash } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email profile offline_access",
  state: randomBytes(16).toString("base64url"),
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();

console.log(url.toString());
Primera especificación
2012
RFCs principales
RFC 6749 / 6750
RFC de PKCE
RFC 7636
Roles
4
Grant moderno
Authorization Code + PKCE
Capa de identidad
OpenID Connect

Por que importa

Para qué sirve OAuth

Acceso delegado

Un usuario concede a una aplicación un acceso limitado y revocable a sus datos sin entregar nunca una contraseña o una credencial a largo plazo.

Permisos por scopes

Cada concesión solicita scopes explícitos, por lo que un client recibe exactamente el acceso que el usuario consintió y nada más.

Un estándar entre proveedores

El mismo flujo funciona con Google, GitHub, Okta y tu propio servidor, lo que significa un único patrón de integración para cada proveedor de identidad.

La imagen completa

Los cuatro roles en una imagen

Un resource owner concede a un client acceso a un resource server, gestionado por un authorization server que emite tokens.

Resource owner

Conceder

El usuario que posee los datos y decide qué aplicación puede acceder a ellos y para qué.

Client

Solicitar

La aplicación que solicita el acceso, identificada por un client id y, cuando puede mantener uno, un secret.

Authorization server

Intermediario

Autentica al usuario, muestra el consentimiento y emite tokens de acceso, de refresco y de identidad.

Resource server

Host

La API que contiene los datos protegidos y solo los devuelve una vez que acepta un access token válido.

HTML5 de un vistazo

Componentes clave

Authorize endpoint

La redirección del navegador donde el usuario se autentica y da su consentimiento.

Authorization code

Un código de corta duración y de un solo uso devuelto a la redirect URI del client.

Token endpoint

Un POST por canal posterior (back-channel) que intercambia un código o un refresh token por tokens.

Redirect URI

El callback registrado, que debe coincidir exactamente para evitar el robo de códigos.

Resource server

La API que acepta un bearer access token y devuelve los datos.

Scopes

Permisos delimitados por espacios que restringen lo que un token puede hacer.

Flujo

Authorization Code + PKCE

El flujo predeterminado para web, móvil y aplicaciones de una sola página. Dos redirecciones y un intercambio por canal posterior.

  1. 1

    Redirección con un challenge

    El client envía el navegador a /authorize con su client id, redirect URI, scopes, state y un code_challenge de PKCE hasheado.

  2. 2

    El usuario consiente

    El authorization server autentica al usuario y muestra los scopes solicitados. El usuario aprueba o deniega.

  3. 3

    Redirección de vuelta con un código

    El navegador regresa a la redirect URI registrada con un authorization code de corta duración y un solo uso, junto con el state original.

  4. 4

    Intercambio de código y verificador

    El client envía un POST con el código y el code_verifier original al token endpoint y recibe los tokens de acceso, refresco e ID.

  5. 5

    Llamada al resource server

    El client envía el access token como una credencial Bearer en las solicitudes a la API. El resource server lo valida y aplica los scopes.

  6. 6

    Refrescar cuando expire

    Cuando el access token caduca, el client intercambia el refresh token por un nuevo par, y luego revoca los tokens al cerrar sesión.

Una breve historia

De las solicitudes firmadas a PKCE

  1. 2007

    OAuth 1.0 y solicitudes firmadas

    Una especificación temprana que firma cada solicitud con secretos compartidos, lo cual es seguro pero tedioso tanto para clients como para proveedores.

    07
  2. 2012

    OAuth 2.0 reemplaza las firmas

    El RFC 6749 introduce los bearer tokens y el modelo basado en roles que sigue definiendo el protocolo hoy en día.

    12
  3. 2014

    OpenID Connect añade identidad

    OIDC añade una capa de ID token y un endpoint de userinfo sobre OAuth, permitiendo el inicio de sesión real.

    14
  4. 2015

    PKCE protege a los clients públicos

    El RFC 7636 cierra la vulnerabilidad de interceptación de códigos para aplicaciones que no pueden guardar un client secret.

    15
  5. 2021

    OAuth 2.1 consolida las lecciones

    El borrador depreca los grants implícitos y de contraseña, y hace que PKCE sea obligatorio, codificando las mejores prácticas.

    21

La guia completa

OAuth 2.0: Todo lo que necesitas saber

Qué es, y qué no es, OAuth 2.0

OAuth 2.0 es un framework de autorización. Permite que un usuario conceda a una aplicación de terceros acceso limitado a sus recursos sin tener que compartir su contraseña. El resultado de un flujo exitoso es un access token, y dicho token describe qué puede hacer el cliente, no necesariamente quién es el usuario.

Esa distinción confunde a la gente constantemente. “Sign in with Google” es OAuth 2.0 más OpenID Connect. La parte de OAuth obtiene acceso a las API de Google; la parte de OpenID Connect devuelve un ID token que identifica realmente al usuario. Si implementas solo OAuth y tratas el access token como una prueba de identidad, habrás construido un acceso delegado y lo habrás llamado autenticación, lo cual es un error de categoría con consecuencias de seguridad.

El protocolo fue diseñado para un problema específico: permitir que un sitio de impresión de fotos lea tus imágenes desde un proveedor de almacenamiento sin llegar a ver nunca tu contraseña de almacenamiento. Ten en cuenta ese origen y las decisiones de diseño cobrarán sentido.

OpenID Connect: identidad sobre la base

OpenID Connect es una capa ligera de identidad sobre OAuth 2.0. Añade el scope openid, un endpoint estándar de userinfo y, lo más importante, un ID token, un JWT cuyos claims describen el evento de autenticación.

{
  "iss": "https://accounts.example.com",
  "sub": "110169484474386276334",
  "aud": "web-app",
  "exp": 1760000000,
  "iat": 1759999100,
  "email": "[email protected]",
  "email_verified": true,
  "nonce": "n-0S6_WzA2Mj"
}

El ID token es para el cliente, no para la API. Nunca lo envíes a un servidor de recursos como si fuera un access token. Verifica su firma, iss, aud, exp y nonce antes de confiar en él, y utiliza sub, no el email, como identificador estable del usuario. Las direcciones de correo electrónico cambian y se reasignan; el subject es estable durante toda la vida de la cuenta.

Los cuatro roles

OAuth define cuatro participantes, y ser precisos con ellos hace que el resto del protocolo sea evidente.

  • Resource owner — el usuario que posee los datos y concede el acceso.
  • Client — la aplicación que solicita el acceso, por ejemplo, tu aplicación web.
  • Authorization server — emite los tokens tras autenticar al usuario y obtener su consentimiento.
  • Resource server — la API que acepta el token de acceso y devuelve los datos.

Tu aplicación web es el client. Google es el authorization server. Las APIs de Google son el resource server. El usuario es el resource owner. A menudo, un mismo proveedor desempeña ambos roles de servidor, razón por la cual la distinción puede parecer académica hasta que construyes la tuya propia.

Tipos de grant

Un grant type es la “receta” que utiliza un cliente para obtener un token. Actualmente, hay cuatro que son importantes.

  • Authorization Code + PKCE — el estándar para aplicaciones web, móviles y single-page apps. El usuario se autentica en el servidor de autorización y el cliente intercambia un código de corta duración por tokens.
  • Client Credentials — comunicación machine-to-machine. No interviene ningún usuario; el cliente se autentica con sus propias credenciales.
  • Device Code — para dispositivos con entrada limitada, como televisores y herramientas de CLI.
  • Refresh Token — no es un método para iniciar sesión, sino la forma estándar de renovar un access token sin necesidad de otra redirección.

Dos grants están prácticamente obsoletos. El flujo implicit devolvía los tokens directamente en el fragmento de la URL, exponiéndolos al historial y a los referrers. El grant de password pedía que el cliente gestionara la contraseña del usuario, lo cual anula el propósito fundamental de OAuth. OAuth 2.1 elimina ambos, y ningún sistema nuevo debería utilizarlos.

Authorization Code + PKCE

Este es el flujo que debes aprender. El cliente redirige al usuario al servidor de autorización con un desafío (challenge) hasheado, el usuario da su consentimiento, el servidor redirige de vuelta con un código de un solo uso y el cliente intercambia ese código junto con el verificador original por los tokens.

El código es inútil sin el verificador, por lo que un atacante que intercepte la redirección no podrá completar el intercambio. Esto es lo que hace que PKCE sea seguro incluso para clientes públicos, como aplicaciones móviles y aplicaciones de una sola página (SPA), que no pueden mantener un secreto en absoluto.

El flujo consta de dos redirecciones del navegador y una solicitud por canal posterior (back-channel). Las redirecciones son visibles y pueden ser manipuladas; el intercambio es un POST directo de servidor a servidor que un atacante no puede observar. Mantener el material secreto en el back channel es la base de todo el diseño.

La URI de redireccionamiento y el estado

La redirect URI es la dirección a la que el servidor de autorización devuelve al usuario. Debe registrarse y coincidir exactamente. Las coincidencias laxas son el origen de vulnerabilidades de redireccionamiento abierto (open-redirect) que filtran códigos de autorización a hosts controlados por atacantes. Nunca aceptes una redirect URI proveniente de un parámetro de consulta y no permitas subdominios con comodines (wildcards).

El parámetro state es un valor opaco que el cliente genera y verifica cuando regresa el callback. Este protege el endpoint del callback contra CSRF. Un atacante que engañe a una víctima para completar un flujo con el código del atacante fallará la verificación del estado.

const state = randomBytes(16).toString("base64url");
session.oauthState = state;

// later, in the callback:
if (query.state !== session.oauthState) {
  throw new Error("state_mismatch");
}

Para OIDC, añade un nonce y verifícalo dentro del ID token. El state protege el callback del cliente contra CSRF; el nonce vincula el ID token a esta solicitud específica y bloquea los ataques de replay.

PKCE en detalle

PKCE (Proof Key for Code Exchange) está definido por el RFC 7636 y ahora es obligatorio para clientes públicos. Se basa en tres valores: el cliente genera un code_verifier aleatorio, deriva un code_challenge como su hash SHA-256, envía el challenge en la solicitud de autorización y envía el verifier en la solicitud del token. El servidor aplica el hash al verifier y los compara.

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

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");

Utiliza siempre el método S256. El método plain existe únicamente por compatibilidad y anula el propósito de PKCE, ya que un atacante que vea el challenge verá también el verifier. Almacena el verifier en la sesión del usuario para que el callback pueda encontrarlo y elimínalo después de un único uso.

Construyendo la solicitud de autorización

La solicitud de autorización es una redirección del navegador, por lo que se trata de un GET con parámetros de consulta (query parameters). El usuario ve la pantalla de consentimiento del proveedor, no tu aplicación.

export function buildAuthorizeUrl(state: string, challenge: string) {
  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();
  return url.toString();
}

response_type=code selecciona el flujo de código de autorización. El scope openid convierte la solicitud en una solicitud OIDC. offline_access es la convención común para solicitar un refresh token, y algunos proveedores lo condicionan a un aviso de consentimiento adicional.

Intercambio de tokens

El callback entrega code y state. Tras verificar el state, el cliente envía el code mediante un POST al endpoint de tokens. Esta es una llamada de back-channel desde tu servidor, no una redirección del navegador.

export async function exchangeCode(code: string, verifier: string) {
  const res = await fetch("https://auth.example.com/token", {
    method: "POST",
    headers: { "content-type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://app.example.com/callback",
      client_id: "web-app",
      code_verifier: verifier,
    }),
  });

  if (!res.ok) throw new Error("token_exchange_failed");
  return res.json();
}

La respuesta contiene access_token, token_type, expires_in, usualmente refresh_token y, en el caso de OIDC, un id_token. Un cliente confidencial, aquel que puede manejar un secret, también se autentica en esta solicitud mediante client_secret o, preferiblemente, una aserción de clave privada. Un cliente público depende exclusivamente de PKCE.

Tokens de acceso, refresco e ID

Estos tres tokens tienen funciones distintas, y confundirlos provoca errores sutiles.

  • Access token — se presenta al servidor de recursos. A menudo es un JWT, pero la especificación solo requiere que sea opaco para el cliente. Puede ser una cadena aleatoria que el servidor busca en su base de datos.
  • Refresh token — se presenta únicamente al servidor de autorización para obtener un nuevo access token. Tiene una vida útil larga y es altamente sensible.
  • ID token — un JWT de OIDC que le indica al cliente quién acaba de iniciar sesión. Nunca se envía a una API.

Trata el access token como una credencial de portador (bearer credential): cualquier persona que lo posea puede utilizarlo. Mantén los tiempos de vida cortos, solicita los scopes más restringidos que necesites y deja que el refresh token gestione la relación a largo plazo. El formato del token en sí se explica en la guía de JWT.

Scopes y consentimiento

Los scopes expresan qué es lo que el cliente está solicitando. El servidor de autorización los muestra al usuario en una pantalla de consentimiento y codifica el subconjunto concedido dentro del access token.

Solicita lo mínimo. Una aplicación de calendario que pida acceso total al buzón de correo asustará a los usuarios y ampliará el radio de impacto en caso de una brecha de seguridad. Los proveedores también publican scopes reservados: openid es requerido para OIDC, y offline_access suele controlar los refresh tokens.

Los scopes no son roles. Un scope describe una capacidad que el cliente solicitó para esta concesión; un rol describe quién es el usuario dentro de tu sistema. Realiza el mapeo entre ambos en tu servidor y nunca asumas que los scopes de un proveedor dicen algo sobre tu propio modelo de autorización. Para esa parte del problema, consulta RBAC.

Llamando al servidor de recursos

Con el token de acceso en mano, las llamadas a la API lo incluyen en el encabezado Authorization utilizando el esquema Bearer.

const res = await fetch("https://api.example.com/me", {
  headers: { authorization: `Bearer ${accessToken}` },
});

El servidor de recursos valida el token, ya sea verificando un JWT localmente o mediante introspección, comprueba el scope y devuelve los datos. Nunca ve el refresh token ni el ID token, y debería rechazarlos en caso de recibirlos.

Introspección y revocación

No todos los access tokens son un JWT. Cuando el token es opaco, el servidor de recursos le pregunta al servidor de autorización si sigue siendo válido mediante la introspección de tokens (token introspection), definida por el RFC 7662.

POST /introspect HTTP/1.1
Host: auth.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <client credentials>

token=2YotnFZFEjr1zCsicMWpAA

La introspección devuelve active, además del scope, el subject y la fecha de expiración. Es un método autoritativo pero requiere una llamada de red, por lo que se recomienda cachear el resultado durante unos pocos segundos.

La revocación (revocation), definida por el RFC 7009, permite que un cliente indique al servidor de autorización que invalide un token, generalmente durante el cierre de sesión (logout).

await fetch("https://auth.example.com/revoke", {
  method: "POST",
  headers: { "content-type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({ token: refreshToken, client_id: "web-app" }),
});

Revoca los refresh tokens de forma agresiva al cerrar sesión, y recuerda que revocar un refresh token no siempre invalida los access tokens que ya han sido emitidos. Lo que hace que la revocación se sienta inmediata es mantener los access tokens con tiempos de vida cortos.

Machine-to-machine: client credentials

Cuando no hay un usuario involucrado, el cliente actúa por sí mismo. Se autentica en el token endpoint y recibe un access token limitado a sus propios permisos.

const res = await fetch("https://auth.example.com/token", {
  method: "POST",
  headers: {
    "content-type": "application/x-www-form-urlencoded",
    authorization: `Basic ${btoa(`${clientId}:${clientSecret}`)}`,
  },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    scope: "reports:read",
  }),
});

No hay pantalla de consentimiento ni refresh token; el servicio simplemente solicita un nuevo token cuando el anterior expira. Prefiere la autenticación de cliente mediante private-key JWT sobre un secreto compartido siempre que el proveedor lo soporte, y rota los secretos periódicamente. Este es el mismo nicho que ocupan las API keys de larga duración, pero con expiración y scoping estándar, como se explica en API Keys.

Cuándo no usar OAuth

OAuth sirve para delegar el acceso a un tercero. Si tu propia aplicación web está autenticando a sus propios usuarios contra su propio backend, no necesitas OAuth. Una cookie de sesión, o un JWT de primera mano emitido por ti mismo, es más simple y más fácil de asegurar. Colocar un servidor de autorización entre tu formulario de inicio de sesión y tu base de datos añade complejidad, no seguridad.

Recurre a OAuth cuando integres un proveedor externo, actúes como proveedor para clientes de terceros o necesites un estándar para el acceso machine-to-machine. De lo contrario, comienza con Session Auth y añade OAuth solo cuando surja una necesidad real de delegación. Debido a que cada flujo aquí se ejecuta mediante redirecciones y headers, la guía de HTTP es un complemento útil.

Errores comunes

  • Implicit flow — los tokens en el fragmento de la URL se filtran a través del historial, los logs y los referrers. Utiliza Authorization Code con PKCE.
  • Tokens en localStorage — un bug de XSS se convierte en el robo de la cuenta. Mantén los tokens en cookies httpOnly o sesiones en el servidor.
  • Open redirects — una validación laxa de la URI de redirección permite que un atacante robe los códigos. Haz que la URI coincida exactamente con la registrada.
  • Omitir el state — sin él, el callback es vulnerable a CSRF.
  • Scopes demasiado amplios — solicitar acceso a todo hace que el consentimiento no tenga sentido y agrava las brechas de seguridad.
  • Access tokens de larga duración — no se pueden revocar rápidamente. Mantenlos cortos y rota los refresh tokens.
  • Usar el ID token como credencial de API — es para el cliente, no para el servidor de recursos.

Elegir el flujo adecuado

La mayoría de las decisiones de integración se resumen en dos preguntas: ¿hay un usuario y puede el cliente mantener un secreto?

  • Hay un usuario presente, el cliente es público (SPA, aplicación móvil, aplicación de escritorio): Authorization Code con PKCE.
  • Hay un usuario presente, el cliente es confidencial (aplicación web con renderizado en servidor): Authorization Code con PKCE más autenticación de cliente.
  • No hay usuario, el cliente es un servicio (cron job, microservicio): Client Credentials.
  • El dispositivo no tiene navegador ni teclado (TV, CLI): Device Code.

Todo lo demás es legado. Si un proveedor solo documenta el flujo implícito, tómalo como una señal de advertencia y verifica si PKCE está disponible.

El patrón backend-for-frontend

El lugar más seguro para almacenar tokens es un servidor que tú controles. El patrón backend-for-frontend coloca un servidor ligero entre el navegador y el servidor de autorización: el navegador recibe una cookie de sesión y el servidor conserva los access y refresh tokens.

app.get("/callback", async (req, res) => {
  if (req.query.state !== req.session.oauthState) {
    return res.status(400).json({ error: "state_mismatch" });
  }

  const tokens = await exchangeCode(
    String(req.query.code),
    req.session.codeVerifier,
  );

  req.session.tokens = tokens;
  delete req.session.oauthState;
  delete req.session.codeVerifier;

  res.redirect("/dashboard");
});

El navegador nunca ve un token, por lo que un ataque XSS no puede robarlo, y el servidor puede realizar el refresh de forma silenciosa. La contrapartida es un salto adicional y un servidor más que mantener, lo cual suele ser más económico que el incidente que se evita.

Clientes nativos y móviles

Las aplicaciones nativas no pueden mantener un client secret, por lo que PKCE no es opcional. También presentan un problema de redirección: un esquema personalizado como myapp://callback puede ser reclamado por una aplicación maliciosa. La solución moderna es utilizar una pestaña del navegador del sistema, ya sea mediante la API de sesión de autenticación de la plataforma o un navegador embebido que comparta cookies con el sistema.

Nunca utilices un WebView embebido para OAuth. La aplicación podría leer la contraseña del usuario, lo que rompería el límite de confianza que el protocolo entero busca proteger. Abre el navegador del sistema, recibe el callback y guarda los tokens en el almacenamiento seguro de la plataforma.

Ejecutar tu propio servidor de autorización

No es necesario que construyas uno. Servidores establecidos como Keycloak, Ory Hydra y proveedores de identidad en la nube ya implementan el protocolo, las pantallas de consentimiento, la gestión de claves y el almacenamiento de tokens por ti. Construir el tuyo propio es un proyecto de varios meses cuyos modos de fallo son todos críticos para la seguridad.

Si decides ejecutar el tuyo propio, las responsabilidades mínimas son: coincidencia exacta de la redirect URI, obligatoriedad de PKCE, tiempos de vida cortos para los access-tokens, rotación de refresh-tokens con detección de reutilización, un endpoint de JWKS y revocación. Si olvidas cualquiera de estos puntos, habrás implementado una vulnerabilidad, no una funcionalidad.

Probando una integración de OAuth

Las pruebas end-to-end de OAuth son lentas y frágiles porque dependen de un proveedor real y de un navegador real. En su lugar, realiza las pruebas por capas.

  • Realiza unit tests de la construcción de la URL, la derivación de PKCE y la comparación de estados como funciones puras.
  • Realiza integration tests del intercambio de tokens contra un servidor de autorización mock que devuelva tokens predefinidos.
  • Mantén un único smoke test contra el proveedor real, marcado para que no se ejecute en cada commit.
test("builds an authorize URL with PKCE", () => {
  const url = new URL(buildAuthorizeUrl("state-1", "challenge-1"));
  expect(url.searchParams.get("response_type")).toBe("code");
  expect(url.searchParams.get("code_challenge_method")).toBe("S256");
  expect(url.searchParams.get("state")).toBe("state-1");
});

El callback es donde suelen esconderse los bugs, así que prueba los flujos de error explícitamente: un estado discordante, un código reutilizado y un token expirado deberían producir cada uno un error claro en lugar de una sesión parcialmente autenticada.

Cierre de sesión y single sign-on

Cerrar sesión en tu aplicación no es lo mismo que cerrar sesión en el proveedor. Un cierre de sesión completo realiza tres acciones: destruye la sesión local, revoca el refresh token en el servidor de autorización y, en el caso de single sign-on, finaliza la sesión del proveedor para que el siguiente inicio de sesión no la reutilice de forma silenciosa.

app.post("/logout", async (req, res) => {
  if (req.session.tokens?.refresh_token) {
    await revoke(req.session.tokens.refresh_token);
  }
  req.session.destroy(() => res.redirect("/"));
});

El single sign-on surge del mismo mecanismo: una vez que un usuario tiene una sesión en el proveedor, los clientes posteriores reciben un código sin necesidad de volver a introducir la contraseña. Esto es conveniente, y es también la razón por la cual el cierre de sesión en computadoras compartidas es más importante de lo que la gente cree.

Rotación de refresh tokens y detección de reutilización

OAuth no exige que los refresh tokens sean de un solo uso, pero las implementaciones más robustas los rotan. Cada solicitud de refresco devuelve un nuevo refresh token e invalida el anterior. Si se presenta un token antiguo nuevamente, el servidor detecta que ha sido robado y revoca toda la familia de tokens.

export async function refresh(grant: { refresh_token: string; client_id: string }) {
  const stored = await db.refreshToken.findByHash(hash(grant.refresh_token));

  if (!stored || stored.revoked) {
    if (stored) await revokeFamily(stored.familyId);
    throw new Error("invalid_grant");
  }

  await db.refreshToken.revoke(stored.id);
  return issueTokens(stored.userId, stored.familyId);
}

La detección de reutilización es la ventaja clave: un refresh token robado se convierte en una trampa en lugar de una puerta trasera permanente. Combínalo con una expiración deslizante (sliding expiry) para que un usuario activo permanezca conectado mientras que un token abandonado termine por expirar.

Mejores prácticas

  • Utiliza Authorization Code con PKCE para cada cliente orientado al usuario, incluyendo SPAs y aplicaciones móviles.
  • Utiliza Client Credentials para comunicaciones machine-to-machine, preferiblemente con autenticación mediante clave privada.
  • Registra URIs de redireccionamiento exactas y rechaza cualquier solicitud que no coincida carácter por carácter.
  • Envía y verifica siempre el state; añade y verifica un nonce para OIDC.
  • Solicita los scopes mínimos y trata la pantalla de consentimiento como un paso significativo.
  • Mantén los access tokens con una vida útil corta, rota los refresh tokens y revócalos al cerrar sesión.
  • Verifica la firma del ID token, iss, aud, exp y nonce antes de confiar en él.
  • Guarda los client secrets y las claves privadas en un gestor de secretos, nunca en el frontend.
  • Almacena en caché la introspección brevemente y confía en los tiempos de vida cortos para una revocación oportuna.

Errores comunes

  • Tratar OAuth como autenticación sin utilizar OpenID Connect.
  • Implementar el implicit flow solo porque requiere una redirección menos.
  • Almacenar access tokens en localStorage y enviarlos a cualquier origen.
  • Incluir el client secret en una single-page app donde cualquiera puede leerlo.
  • Aceptar cualquier redirect URI, o una proporcionada a través de un query parameter.
  • Olvidar validar state en el callback.
  • Solicitar todos los scopes posibles y nunca revisar la lista.
  • Asumir que revocar un refresh token anula instantáneamente los access tokens.
  • Utilizar el ID token como bearer token para tu API.

Próximos pasos

OAuth es la forma en que se obtienen los tokens; JWT explica cómo se construyen y verifican. Si tu aplicación es de primera mano (first-party) y necesitas revocación inmediata, Session Auth es el punto de partida más sencillo. Para clientes automáticos que no involucran a un usuario, API Keys cubre la alternativa de larga duración. Y dado que cada uno de estos flujos se ejecuta a través de la red, vale la pena darle un repaso a HTTP.

En la practica

Autorizar, intercambiar, llamar, refrescar

El ciclo de vida completo del authorization code en cuatro solicitudes.

authorize.ts
import { randomBytes, createHash } from "node:crypto";

export function buildAuthorizeUrl(session: { state?: string }) {
  const verifier = randomBytes(32).toString("base64url");
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  const state = randomBytes(16).toString("base64url");

  session.state = state;
  session.codeVerifier = verifier;

  const url = new URL("https://auth.example.com/authorize");
  url.search = new URLSearchParams({
    response_type: "code",
    client_id: "web-app",
    redirect_uri: "https://app.example.com/callback",
    scope: "openid email profile offline_access",
    state,
    code_challenge: challenge,
    code_challenge_method: "S256",
  }).toString();

  return url.toString();
}

Authorization Code + PKCE vs Implicit

PKCE mantiene los tokens fuera de la URL y del historial del navegador, y funciona para clients públicos que no pueden guardar un secreto.

Preferir
// Browser is redirected, then the backend exchanges the code.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
}).toString();
Evitar
// Tokens land in the URL fragment where history,
// logs and referrers can leak them.
const url = new URL("https://auth.example.com/authorize");
url.search = new URLSearchParams({
  response_type: "token",
  client_id: "web-app",
  redirect_uri: "https://app.example.com/callback",
  scope: "openid email",
}).toString();

scopes vs roles

Un scope es lo que el client solicitó en esta concesión. Un rol es lo que el usuario es dentro de tu sistema. No confundas ambos.

Scope
// Requested capability, granted through consent.
const scope = "reports:read reports:export";

if (!req.user.scope.includes("reports:read")) {
  return res.status(403).json({ error: "insufficient_scope" });
}
Rol
// Position in your own authorization model.
const role = "analyst";

// A provider scope never substitutes for a local
// role check; map scopes to roles on your server.
if (!["analyst", "admin"].includes(req.user.role)) {
  return res.status(403).json({ error: "forbidden" });
}

Compromisos

¿Deberías construir sobre OAuth?

OAuth es la herramienta adecuada para la delegación e integración, pero la herramienta equivocada para el login propio (first-party).

Strengths

  • Sin compartir contraseñas

    Los usuarios conceden acceso limitado a través del proveedor en el que ya confían y pueden revocarlo sin cambiar sus credenciales.

  • Una integración, muchos proveedores

    El mismo flujo de authorization code funciona entre proveedores, por lo que añadir un segundo proveedor de identidad es mayormente configuración.

  • Revocable por diseño

    Los access y refresh tokens pueden ser revocados, y la corta vida de los access tokens limita el impacto de una filtración.

Trade-offs

  • No es autenticación

    OAuth por sí solo prueba que un client puede acceder a un recurso, no quién es el usuario. Necesitas OpenID Connect para la identidad.

  • Errores críticos fáciles de cometer

    Redirect URIs laxas, falta de state y tokens almacenados en el navegador conducen directamente al robo de cuentas si se omiten.

  • La complejidad tiene un coste

    Ejecutar tu propio authorization server implica gestión de claves, pantallas de consentimiento, almacenamiento de tokens y revisiones de seguridad constantes.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender OAuth 2.0?

Nuestro tutorial interactivo te guia a traves de OAuth 2.0 paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.