Blog de Obi Madu
Volver a todos los artículos
System DesignInfrastructureBackend

OAuth vs OIDC: La Diferencia Explicada por Fin

Una explicación práctica de OAuth, OIDC, access tokens e ID tokens sin la habitual confusión sobre autenticación.

OAuth vs OIDC: La Diferencia Explicada por Fin

OAuth y OpenID Connect son dos tecnologías profundamente fundamentales que los desarrolladores modernos usan constantemente, sin embargo, a menudo las describen de manera imprecisa. La gente dice casualmente cosas como "inicio de sesión con OAuth" o "iniciar sesión de forma segura con OAuth". A veces, ese fraseo relajado es solo una jerga inofensiva de la industria, pero otras veces oculta críticamente una distinción arquitectónica increíblemente importante. OAuth 2.0 es fundamentalmente un marco construido completamente alrededor de la autorización. OpenID Connect, casi universalmente abreviado como OIDC, es una capa de identidad estrictamente construida para la autenticación.

Esa diferencia semántica suena puramente académica hasta que estás construyendo una aplicación real y lista para producción. Si operas utilizando el modelo mental incorrecto, puedes terminar fácilmente tratando un access token estándar como prueba definitiva de la identidad del usuario, saltándote peligrosamente pasos críticos de verificación de tokens, almacenando tokens altamente sensibles de manera insegura en dispositivos móviles o asumiendo tontamente que todos los proveedores de identidad se comportan exactamente como Google.

La versión absolutamente más simple de esta distinción es esta: OAuth responde firmemente a la pregunta crucial: "¿Qué se le permite hacer exactamente a esta aplicación específica?". Mientras tanto, OIDC responde claramente a la pregunta completamente diferente: "¿Quién es precisamente este usuario específico?". OIDC está literalmente construido directamente sobre el marco establecido de OAuth 2.0, agregando sin problemas una capa de identidad robusta y altamente estandarizada al flujo de autorización existente de OAuth.

La Analogía de la Pulsera y la Tarjeta de Identificación

Imagina intentar entrar en un club nocturno exclusivo.

OAuth es esencialmente como recibir una pulsera de colores brillantes en la puerta. Le dice claramente al portero y al personal del lugar exactamente a qué áreas se te permite acceder formalmente. Tal vez tu pulsera signifique que puedes entrar a la sección VIP acordonada, o quizás indique que tienes permiso para pedir directamente en la barra premium. En todos los sentidos, la pulsera representa únicamente un permiso concedido. Pero de manera crucial, la pulsera no prueba absolutamente quién eres en realidad. No muestra tu nombre legal, le falta tu foto, no confirma tu edad y no contiene ningún número de identidad oficial.

OIDC es precisamente como recibir esa misma pulsera y, al mismo tiempo, verse obligado a llevar una tarjeta de identificación gubernamental altamente confiable. Aún conservas todos tus permisos concedidos a través de la pulsera, pero ahora también posees un documento de identidad estandarizado y ampliamente confiable que establece definitivamente quién eres realmente.

Ese escenario es exactamente lo que sucede técnicamente en el mundo del software:

OAuth 2.0 gives you:
  Access token
  Used to call APIs

OIDC gives you:
  Access token
  Used to call APIs

  ID token
  Used to identify the user

El access token y el ID token son hermanos funcionales. Un token no es en absoluto un sustituto o un reemplazo del otro.

Lo Que OAuth 2.0 Hace Realmente

En su núcleo absoluto, OAuth 2.0 simplemente permite que un usuario otorgue de forma segura a una aplicación de terceros un permiso explícito para acceder a un recurso fuertemente protegido en su nombre. Un ejemplo muy común en el mundo real es conectar una nueva aplicación a tu cuenta existente de GitHub. La aplicación redirige sin problemas al usuario directamente a GitHub y solicita explícitamente acceso a repositorios específicos.

https://github.com/login/oauth/authorize?
  client_id=abc123&
  scope=repo&
  redirect_uri=https://myapp.com/callback

GitHub posteriormente presenta al usuario una pantalla clara de consentimiento. Si el usuario aprueba afirmativamente la solicitud, GitHub lo redirige de forma segura a la aplicación original, llevando un código de autorización temporal.

https://myapp.com/callback?code=xyz789

Detrás de escena, la aplicación intercambia de forma segura ese código temporal por un access token mucho más duradero.

POST https://github.com/login/oauth/access_token
Content-Type: application/x-www-form-urlencoded

client_id=abc123&
client_secret=secret456&
code=xyz789&
redirect_uri=https://myapp.com/callback

La respuesta del proveedor contiene de manera importante el access token recién creado.

{
  "access_token": "gho_16C7e42F292c6912E7710c838347Ae178B4a",
  "token_type": "bearer",
  "scope": "repo"
}

Ese token específico ahora puede usarse repetidamente para llamar a las APIs de GitHub altamente protegidas en nombre del usuario.

GET https://api.github.com/user/repos
Authorization: Bearer gho_16C7e42F292c6912E7710c838347Ae178B4a

Nota cuidadosamente lo que OAuth fundamentalmente no nos dio a lo largo de todo este proceso. Absolutamente no nos dio ningún tipo de token de identidad estándar. Estrictamente nos dio permiso con un alcance muy específico para llamar a una API. Específicamente con GitHub, si realmente deseas determinar la identidad del usuario, te ves obligado a usar ese access token para llamar manualmente a una API de identidad explícita, como GET /user. Ese paso adicional existe precisamente porque el flujo de inicio de sesión más común de GitHub utiliza OAuth 2.0 puro, en lugar de OIDC.

Lo Que OIDC Añade

OIDC comienza de manera inteligente exactamente con el mismo flujo fundacional de OAuth, pero agrega explícitamente un alcance de identidad altamente requerido y estandarizado llamado openid. Por ejemplo, una solicitud típica de inicio de sesión de Google casi siempre incluye un grupo de alcances (scopes) que se ve exactamente así:

scope=openid profile email

Ese simple alcance openid actúa como la señal definitiva al proveedor de que se trata formalmente de un flujo de OpenID Connect. La aplicación solicitante está declarando explícitamente que no solo está pidiendo acceso estándar a la API; está pidiendo activamente al proveedor subyacente que autentique rigurosamente al usuario y luego devuelva su información de identidad en un formato altamente estandarizado y reconocido globalmente.

Después de que finaliza el intercambio estándar de código, un proveedor compatible con OIDC predeciblemente devuelve tanto un access token como un ID token simultáneamente.

{
  "access_token": "ya29.a0AfH6SMBxC...",
  "id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE2...",
  "refresh_token": "1//04dGKC8...",
  "expires_in": 3600,
  "token_type": "Bearer"
}

El access token permanece firmemente designado exclusivamente para llamar a las APIs. El ID token recién introducido funciona como el documento de identidad altamente confiable. De manera crucial, el ID token casi siempre está formateado como un JWT (JSON Web Token), lo que dicta estrictamente que posee tres partes distintas: un encabezado, una carga útil (payload) y una firma criptográfica.

header.payload.signature

La carga útil central contiene detalles profundos (claims) sobre el usuario específico y las circunstancias exactas del evento de autenticación en sí.

{
  "sub": "109651783456723954281",
  "name": "John Doe",
  "email": "john.doe@gmail.com",
  "email_verified": true,
  "picture": "https://lh3.googleusercontent.com/a-/abc123",
  "iss": "https://accounts.google.com",
  "aud": "1234567890-abcd1234.apps.googleusercontent.com",
  "iat": 1712345678,
  "exp": 1712349278
}

El claim sub sirve consistentemente como el ID de usuario único altamente estable del lado del proveedor. El claim iss te informa de manera confiable exactamente quién emitió el token de forma segura. El claim aud establece definitivamente para qué aplicación cliente específica se emitió intencionalmente el token. El claim exp define claramente exactamente cuándo caduca matemáticamente el token. Sin embargo, esos claims integrados son completamente inútiles y potencialmente peligrosos a menos que primero verifiques activa y correctamente la integridad criptográfica del token.

Nunca Simplemente Decodifiques un ID Token

Un error increíblemente común y peligroso de principiante es dividir perezosamente el JWT, decodificar el payload en base64 y confiar ciegamente en cualquier texto que resulte contener. Ese proceso imprudente no es en absoluto una verificación; es simplemente un análisis de texto.

Un JWT solo es genuinamente útil específicamente porque está firmado criptográficamente por el emisor. Tu backend seguro debe verificar rigurosamente esa firma integrada contra las claves alojadas públicamente por el proveedor de identidad. Además, debe validar fuertemente al emisor, verificar estrictamente la audiencia, hacer cumplir el tiempo de expiración y validar agresivamente cualquier otro claim específico del que dependa críticamente tu aplicación.

La lista de verificación estándar de verificación del backend es notablemente simple en su concepto central:

VerificaciónPor qué importa
FirmaPrueba que el token realmente provino del proveedor de confianza
Emisor (Issuer)Evita aceptar tokens falsificados completamente del proveedor equivocado
AudienciaEvita aceptar accidentalmente tokens válidos destinados a otra aplicación
ExpiraciónEvita estrictamente la reutilización maliciosa de tokens viejos y caducados
Verificación de emailEvita efectivamente confiar ciegamente en direcciones de correo electrónico no verificadas y potencialmente falsas

Una versión altamente simplificada de una función de verificación segura del backend generalmente se ve algo así:

def verify_token(token):
    public_key = fetch_provider_public_key(token)

    payload = jwt.decode(
        token,
        public_key,
        algorithms=["RS256"],
        audience=YOUR_CLIENT_ID,
        issuer="https://accounts.google.com",
    )

    if not payload.get("email_verified"):
        raise ValueError("Email is not verified")

    return payload

En un entorno de producción real, invariablemente también necesitas mecanismos robustes de almacenamiento en caché de JWKS, lógica sofisticada de manejo de rotación de claves y reglas de validación profundamente específicas del proveedor. La idea general más importante es que la verificación de identidad robusta pertenece fundamentalmente de manera segura al backend, y absolutamente no simplemente a los payloads de tokens decodificados en el frontend y fácilmente manipulables.

Las Diferencias Entre Proveedores Importan

Es vital comprender que no todos los botones "Iniciar sesión con X" utilizan realmente OIDC bajo el capó. Proveedores masivos como Google, Apple, Microsoft, Auth0, Keycloak y Zitadel, junto con numerosas plataformas modernas de identidad, soportan completamente el estándar OIDC. En esos flujos altamente modernos, puedes esperar de manera confiable recibir un ID token estándar y verificable.

Sin embargo, el flujo de aplicación OAuth más utilizado por GitHub es claramente diferente. Por lo general, solo proporciona un access token. Para adquirir realmente la información del perfil del usuario, te ves obligado a usar ese token para llamar a la API de GitHub manualmente.

const userResponse = await fetch("https://api.github.com/user", {
  headers: {
    Authorization: `Bearer ${accessToken}`,
  },
});

const user = await userResponse.json();

Ese enfoque no es necesariamente peor de manera inherente, pero es innegablemente diferente. El OAuth puro todavía puede usarse con absoluto éxito en flujos de inicio de sesión modernos, pero el mecanismo real de recuperación de identidad se vuelve completamente específico del proveedor a menos que el proveedor elegido soporte explícitamente el estándar OIDC. Esta distinción matizada afecta en gran medida las estrategias de vinculación de cuentas también. Con OIDC verdadero, el claim sub actúa consistentemente como la clave de identidad altamente estable para ese proveedor específico. Con proveedores de solo OAuth, sin embargo, debes recuperar manualmente y almacenar con cuidado el ID de usuario único del proveedor derivado directamente de su respuesta de API personalizada.

Auth Gestionada vs OIDC Autoalojado

En el mundo real del desarrollo de aplicaciones, casi siempre te enfrentas a una elección entre utilizar un proveedor de autenticación completamente gestionado o implementar meticulosamente una integración OIDC robusta por ti mismo.

Los servicios gestionados como Clerk, Auth0 y Firebase Auth existen específicamente para abstraer y ocultar la gran mayoría de los agobiantes detalles del protocolo. Manejan sin esfuerzo la densa configuración del proveedor, los intercambios de tokens complejos, la actualización agresiva de tokens, la gestión segura de sesiones, los registros de usuarios persistentes y los SDKs profundamente integrados. Ese enfoque con "baterías incluidas" es increíblemente atractivo cuando la pura velocidad de desarrollo realmente importa.

Por el contrario, los proveedores de identidad autoalojados como Zitadel o Keycloak ofrecen un control significativamente más granular. Con frecuencia son una opción muy superior para requisitos estrictos de soberanía de datos, cumplimiento normativo intenso, personalización profunda del flujo de trabajo o ahorros masivos de costos a escala empresarial. Sin embargo, la contrapartida es que eres fuertemente responsable de una porción mucho mayor del esfuerzo de integración.

Con un proveedor gestionado, tu backend puede simplemente depender de la verificación de un token de sesión ligero generado explícitamente por el SDK personalizado de ese proveedor.

async def require_auth(request):
    request_state = clerk.authenticate_request(
        request={"headers": dict(request.headers), "url": str(request.url)}
    )

    if not request_state.is_signed_in:
        raise HTTPException(status_code=401)

    return request_state.payload

Con un proveedor OIDC autoalojado, normalmente eres completamente responsable de configurar manualmente el endpoint de autorización, el endpoint de token, el client ID, la URI de redireccionamiento, los alcances solicitados y la intensa lógica de verificación JWT del backend completamente desde cero. Ninguno de estos enfoques específicos es universalmente correcto o incorrecto. La autenticación gestionada reduce drásticamente la carga operativa, mientras que el autoalojamiento aumenta dramáticamente el control a largo plazo.

Las Apps Móviles Necesitan PKCE

Es absolutamente crucial darse cuenta de que las aplicaciones móviles fundamentalmente no son exactamente lo mismo que las aplicaciones web tradicionales renderizadas en un servidor. Una aplicación web tradicional puede almacenar de manera segura y permanente un secreto de cliente altamente sensible completamente en el servidor backend. Una aplicación móvil nativa absolutamente no puede. Literalmente, cualquier cosa compilada y enviada dentro de un archivo APK o IPA público puede ser extraída trivialmente por un usuario medianamente determinado o un script automatizado. Esa cruda realidad significa que las aplicaciones móviles nativas deben tratarse inequívocamente como "clientes públicos" altamente vulnerables.

Esta vulnerabilidad flagrante es exactamente por qué todos los flujos móviles de OAuth y OIDC deberían utilizar rigurosamente PKCE, que significa Proof Key for Code Exchange (Clave de Prueba para el Intercambio de Código). PKCE añade ingeniosamente un secreto temporal y altamente dinámico por cada inicio de sesión al flujo. La aplicación móvil genera localmente un code_verifier aleatorio, lo aplica un hash seguro para convertirlo en un code_challenge enviado durante la solicitud de autorización inicial, y más tarde prueba de manera concluyente que conoce el verificador original cuando finalmente intercambia el código por tokens reales.

1. App creates a random code_verifier
2. App hashes it into a code_challenge
3. Authorization request includes the code_challenge
4. Provider returns an authorization code
5. Token request includes the original code_verifier
6. Provider checks that it matches the challenge

En el popular ecosistema de Expo, esta compleja danza casi siempre se maneja limpiamente de forma directa a través de la biblioteca expo-auth-session.

const [request, response, promptAsync] = AuthSession.useAuthRequest(
  {
    clientId: CLIENT_ID,
    scopes: ["openid", "profile", "email"],
    redirectUri,
    usePKCE: true,
  },
  discovery
);

Además, las aplicaciones móviles necesitan desesperadamente un almacenamiento local de tokens increíblemente seguro. Nunca debes depositar tokens altamente sensibles en un almacenamiento asíncrono no encriptado y fácilmente legible. En un entorno Expo, siempre debes utilizar rigurosamente expo-secure-store para bloquear cualquier material de token sensible.

import * as SecureStore from "expo-secure-store";

await SecureStore.setItemAsync("id_token", idToken);
const savedToken = await SecureStore.getItemAsync("id_token");

Las Tres Capas de Tokens

Una fuente masiva y persistente de inmensa confusión es el simple hecho de que las aplicaciones modernas casi siempre hacen malabarismos con mucho más de un solo tipo de token simultáneamente. El proveedor de identidad externo puede emitir fácilmente tanto un ID token como un access token separado. En respuesta, tu backend interno puede entonces crear dinámicamente su propio token de sesión propietario. Para complicar las cosas, el proveedor también podría emitir simultáneamente un refresh token de larga duración.

Esos tokens tan variados sirven a propósitos completamente diferentes y altamente especializados dentro de la arquitectura.

TokenEmisorPropósito
ID tokenProveedor de identidadPrueba definitivamente quién es exactamente el usuario
Access tokenProveedor de identidad o APIAutoriza estrictamente las llamadas a APIs dependientes
Token de sesión de AppTu app o plataforma AuthAutentica solicitudes continuas directamente a tu backend
Refresh tokenProveedorAdquiere de forma segura tokens nuevos de corta duración cuando caducan los antiguos

Un flujo arquitectónico altamente común y robusto generalmente se ve algo así:

Google ID token -> Your backend verifies it -> Your backend creates an app session

Después de ese intercambio inicial, tu aplicación cliente simplemente puede utilizar tu propio token de sesión fuertemente personalizado para llamar eficientemente a tu API interna. Absolutamente no necesita transmitir continuamente el masivo ID token de Google en cada solicitud de red, a menos que así sea específicamente como fue diseñada intencionalmente tu peculiar arquitectura.

Malentendidos Comunes

El error más común y generalizado en la industria es declarar casualmente que OAuth significa literalmente "iniciar sesión". OAuth ciertamente puede utilizarse mucho para participar de forma segura en un flujo de inicio de sesión más amplio, pero OAuth en sí mismo se trata estricta y enteramente de autorización delegada. OIDC es la capa de identidad real y fuertemente estandarizada construida específicamente para iniciar la sesión de las personas.

Otro error masivo y frecuentemente encontrado es intentar usar el ID token para autorizar directamente llamadas a la API. El ID token existe exclusivamente para que el cliente frontend o el servidor backend comprendan de manera definitiva la identidad del usuario. El access token existe exclusivamente para una autorización robusta de la API. Difuminar esas dos líneas conduce constantemente a fallas arquitectónicas severas.

El comportamiento de cerrar sesión representa otra área increíblemente sutil de confusión. Cerrar la sesión de un usuario de tu aplicación específica por lo general solo borra tu sesión local de la aplicación y elimina furiosamente los tokens locales. Absolutamente no fuerza necesariamente al usuario a cerrar la sesión de Google, Apple o cada una de las otras aplicaciones conectadas que se encuentran en su dispositivo físico. Esa firme separación del estado de la sesión es completamente normal y esperada.

Finalmente, los alcances (scopes) de permisos solicitados siempre deben mantenerse lo más mínimos humanamente posibles. Si tu aplicación solo necesita realmente conocer la identidad del usuario, solo deberías solicitar los alcances openid profile email. Absolutamente nunca debes pedir casualmente un acceso amplio a su calendario, unidad privada, fotos personales u otros permisos amplios a menos que tu aplicación realmente los requiera fundamentalmente para funcionar.

El Resumen Práctico

OAuth 2.0 es fundamentalmente el marco de permisos. Permite definitivamente que una aplicación dada haga algo increíblemente específico en nombre de un usuario. OIDC es la capa de identidad estandarizada que se asienta directamente encima. Permite de manera segura a una aplicación saber de forma confiable exactamente quién es realmente el usuario en una forma universalmente estándar.

Usa el access token exclusivamente para llamar a las APIs. Usa el ID token exclusivamente para verificar la identidad. Siempre verifica rigurosamente los tokens de manera directa en el backend. Siempre aplica agresivamente PKCE para aplicaciones móviles nativas. Siempre almacena los tokens sensibles en un almacenamiento altamente seguro y encriptado. Siempre trata las diferencias de los proveedores de identidad como restricciones arquitectónicas altamente reales, no simplemente como variaciones cosméticas.

Si no recuerdas absolutamente nada más de este artículo, oblígate a recordar esta única regla férrea: OAuth simplemente le da permiso a tu aplicación, pero OIDC definitivamente le da identidad a tu aplicación.

Referencias