Publicado en

OAuth2 y OpenID Connect: la diferencia clave

Icono de llave de valet parking junto a los textos OAuth2 y OpenID Connect sobre fondo oscuro

En el artículo sobre JWT vimos cómo un servidor firma un token para reconocer a un usuario que ha hecho login directamente contra tu propia aplicación. Pero el botón «Continuar con Google» resuelve un problema distinto: tu backend nunca ve la contraseña del usuario, y aun así necesita saber quién es y qué puede hacer en su nombre. Ese problema lo resuelven dos estándares que casi siempre se nombran juntos —OAuth2 y OpenID Connect— y que, aunque están relacionados, no hacen lo mismo.

OAuth2 no es un sistema de login

OAuth2 es un framework de autorización delegada: permite que una aplicación acceda a un recurso en nombre de un usuario, sin que ese usuario le entregue su contraseña. No verifica identidades — solo concede permisos limitados.

La analogía habitual es la llave de valet parking. Le das al aparcacoches una llave que arranca el coche y lo mueve unos metros, pero no abre la guantera ni el maletero. El aparcacoches nunca tiene tu llave original ni sabe nada de ti: solo tiene acceso a una función concreta, durante un tiempo limitado. OAuth2 hace exactamente eso con datos: tu aplicación puede leer los contactos de un usuario, publicar en su nombre, o acceder a sus repositorios, sin haber visto jamás su contraseña.

Los cuatro actores

  • Resource Owner: el usuario. Es quien posee los datos y decide qué permisos concede.
  • Client: tu aplicación. Es quien quiere acceder a esos datos en nombre del usuario.
  • Authorization Server: el servicio que autentica al usuario y emite los tokens (Google, GitHub, Auth0…).
  • Resource Server: la API que finalmente guarda los datos y acepta el token como prueba de acceso.

En muchos casos, el Authorization Server y el Resource Server son el mismo proveedor — Google autentica y también sirve la API de Gmail — pero conceptualmente son dos roles distintos.

El flujo Authorization Code

Diagrama del flujo Authorization Code de OAuth2 con cuatro carriles: usuario, aplicación, servidor de autorización y API, con seis pasos numerados

Es el flujo más usado y el más seguro para aplicaciones con backend propio. Tu aplicación redirige al usuario al Authorization Server con una URL que indica qué permisos pide (scope) y a dónde volver (redirect_uri):

GET https://auth.proveedor.com/authorize?
  client_id=tu_app_id
  &redirect_uri=https://tuapp.com/callback
  &scope=profile email
  &response_type=code
  &state=xyz123

El usuario inicia sesión (si no lo ha hecho ya) y ve una pantalla de consentimiento: «Tu App quiere acceder a tu perfil y tu email». Si acepta, el Authorization Server lo redirige de vuelta con un código de un solo uso:

GET https://tuapp.com/callback?code=AUTH_CODE_TEMPORAL&state=xyz123

Aquí viene el paso que marca la diferencia de seguridad: el backend de tu aplicación — nunca el navegador — intercambia ese código por un token, usando además un client_secret que solo tu servidor conoce:

POST https://auth.proveedor.com/token
{
  "code": "AUTH_CODE_TEMPORAL",
  "client_id": "tu_app_id",
  "client_secret": "••••••",
  "redirect_uri": "https://tuapp.com/callback",
  "grant_type": "authorization_code"
}
 
→ 200 OK
{
  "access_token": "ya29.a0Af...",
  "id_token": "eyJhbGci...",
  "expires_in": 3600
}

El código intermedio existe por una razón concreta: el token de acceso nunca viaja por el navegador del usuario, donde podría quedar expuesto en el historial o en logs. Solo viaja un código de un solo uso, de corta duración, que además solo sirve si quien lo canjea conoce el client_secret.

De dónde sale la identidad: OpenID Connect

OAuth2 por sí solo no te dice quién es el usuario, solo que consiguió acceso. Ese hueco lo llena OpenID Connect (OIDC), una capa de identidad construida encima de OAuth2. Cuando un proveedor soporta OIDC, la respuesta del intercambio de código incluye, además del access_token, un id_token.

Aquí es donde conecta con el artículo anterior: el id_token es, literalmente, un JWT firmado por el proveedor, con la misma estructura de header, payload y signature que ya vimos. Solo que en vez de emitirlo tu propio backend, lo emite el proveedor, y en vez de claims arbitrarios, trae claims de identidad estandarizados:

{
  "iss": "https://auth.proveedor.com",
  "sub": "110169484474386276334",
  "email": "ana@ejemplo.com",
  "email_verified": true,
  "name": "Ana García",
  "exp": 1734654290
}

Tu aplicación verifica la firma de ese JWT igual que verificaría uno propio, con la diferencia de que la clave pública para comprobarla la publica el proveedor en un endpoint conocido (.well-known/openid-configuration).

access_token e id_token no son intercambiables

Comparación entre access_token e id_token: formato, qué demuestran y quién los recibe

Es el error más común al integrar OAuth2 por primera vez:

  • access_token: demuestra que tienes permiso para llamar a una API concreta. Es lo que envías al Resource Server. Puede ser opaco o un JWT, según el proveedor.
  • id_token: demuestra quién es el usuario. Es siempre un JWT. Es para que lo consuma tu propia aplicación — nunca para enviarlo a una API externa.

Enviar el id_token a una API pensando que es un access_token no funciona: la API lo rechaza, porque no fue emitido para ella — su claim aud no coincide con lo que la API espera.

¿Cuándo usar esto en vez de tu propio JWT?

Si controlas tanto el cliente como la API y solo necesitas que los usuarios inicien sesión con un email y contraseña que tú gestionas, el JWT propio que vimos en el artículo anterior es más simple y suficiente. No necesitas Authorization Server ni flujo de redirección.

OAuth2 y OIDC aportan valor cuando aparece alguno de estos casos:

  • Login delegado: quieres que el usuario entre con una cuenta que ya tiene, sin gestionar tú sus contraseñas.
  • Acceso de terceros: una aplicación externa necesita acceder a datos de tu API en nombre de un usuario, con permisos limitados y revocables (scopes).
  • Single sign-on: varios servicios internos comparten una única sesión gestionada por un proveedor central.

Buenas prácticas

  • Usa Authorization Code con PKCE: en SPAs y apps móviles no hay forma segura de guardar un client_secret; PKCE sustituye ese secreto por un valor generado en el momento, sin perder seguridad.
  • Valida siempre el state: es lo que evita que un atacante fuerce a tu aplicación a canjear un código que no pidió.
  • No mezcles tokens: nunca envíes un id_token como si fuera un access_token, ni al revés.
  • Verifica emisor y audiencia del id_token: comprobar la firma no basta — el claim iss debe ser el proveedor esperado y aud debe ser tu client_id.
  • Pide solo los scopes que necesitas: cada scope adicional es acceso que un token robado también tendría.

Con la identidad y los permisos resueltos, queda un problema que aparece en cualquier API con tráfico real: qué pasa cuando la misma petición llega dos veces, o cuando alguien la repite miles de veces por segundo.