Inicio de Sesión OAuth 2.0
Cómo implementar autenticación OAuth 2.0 con Google, GitHub y otros proveedores. Cubre el flujo Authorization Code con PKCE, validación de state y refresh tokens.
Visión General
OAuth 2.0 es el estándar de la industria para autorización delegada, definido en el RFC 6749. Permite que los usuarios inicien sesión con cuentas existentes (Google, GitHub, Microsoft) sin exponer sus contraseñas a tu aplicación. Acá tenés el flujo Authorization Code con PKCE en Python, JavaScript y Java, con validación de state y refresh de tokens.
Cuándo Usar
Usá este recurso cuando:
- Querés ofrecer “Iniciar sesión con Google / GitHub” en tu plataforma. Como alternativa sin contraseña que se ahorra el viaje al proveedor, mirá los Magic Links.
- Necesitás acceder a datos del usuario desde APIs de terceros en su nombre.
- Querés reducir la fatiga de contraseñas y el riesgo de almacenar credenciales.
- Estás construyendo un SaaS donde los clientes enterprise esperan SSO.
Solución
Python (Flask + Authlib)
import os
from flask import Flask, redirect, session, url_for
from authlib.integrations.flask_client import OAuth
app = Flask(__name__)
app.secret_key = os.environ["FLASK_SECRET_KEY"]
oauth = OAuth(app)
google = oauth.register(
name="google",
client_id=os.environ["GOOGLE_CLIENT_ID"],
client_secret=os.environ["GOOGLE_CLIENT_SECRET"],
access_token_url="https://oauth2.googleapis.com/token",
authorize_url="https://accounts.google.com/o/oauth2/auth",
api_base_url="https://www.googleapis.com/oauth2/v1/",
client_kwargs={"scope": "openid email profile"},
)
@app.route("/login")
def login():
redirect_uri = url_for("callback", _external=True)
# Authlib genera y guarda el `state` automáticamente
return google.authorize_redirect(redirect_uri)
@app.route("/callback")
def callback():
token = google.authorize_access_token() # valida `state`, intercambia el code
user = google.get("userinfo").json()
session["user"] = user
return redirect("/dashboard")
JavaScript (Express + Passport)
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const GoogleStrategy = require("passport-google-oauth20").Strategy;
passport.use(
new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback",
state: true, // genera y valida el parámetro state por vos
pkce: true,
},
(accessToken, refreshToken, profile, done) => {
// Buscá o creá el usuario en tu DB acá
return done(null, profile);
}
)
);
passport.serializeUser((user, done) => done(null, user.id));
passport.deserializeUser((id, done) => done(null, { id }));
const app = express();
// express-session es obligatorio — passport.session() lee de ahí
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: { secure: true, httpOnly: true, sameSite: "lax" },
}));
app.use(passport.initialize());
app.use(passport.session());
app.get("/auth/google", passport.authenticate("google", { scope: ["profile", "email"] }));
app.get(
"/auth/google/callback",
passport.authenticate("google", { failureRedirect: "/login" }),
(req, res) => res.redirect("/dashboard")
);
Java (Spring Security + OAuth2 Client)
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.defaultSuccessUrl("/dashboard", true)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
);
return http.build();
}
}
Spring Security maneja state y PKCE automáticamente cuando configurás el client registration en application.yml.
Explicación
El flujo Authorization Code corre en cuatro pasos:
- Redirect: tu app manda al usuario a la URL de autorización del proveedor con
client_id,redirect_uri,scopey unstatealeatorio. - Consentimiento: el usuario inicia sesión en el proveedor y aprueba los scopes pedidos.
- Callback: el proveedor redirige de vuelta con un
codede autorización. - Intercambio de token: tu backend cambia el
code(más elcode_verifiersi PKCE está activo) por unaccess_tokeny unid_token.
PKCE agrega un verificador secreto para que un código interceptado sea inútil sin él — obligatorio para SPAs y mobile, y suficientemente barato para activarlo en todos lados. state ata el callback a la petición que lo inició; sin él, el endpoint de callback queda abierto a ataques de login CSRF. La Best Current Practice de seguridad de OAuth 2.0 (RFC 9700) cubre los dos y todo lo demás que de otro modo aprenderías a las malas.
Variantes
| Flujo | Caso de uso | ¿Client secret? | ¿PKCE? |
|---|---|---|---|
| Authorization Code | Apps web server-side | Sí | Recomendado |
| Authorization Code + PKCE | SPAs, apps móviles | No | Obligatorio |
| Client Credentials | Máquina a máquina | Sí | No |
| Device Code | TVs, herramientas CLI | No | No |
El flujo Implicit está deprecado. Devolvía tokens en el fragmento de la URL donde cualquier script podía leerlos, y el RFC 9700 dice explícitamente que no lo uses.
Lo que Funciona
- Activá PKCE aun en apps server-side. Es un flag y elimina toda la clase de ataques de intercepción de código.
- Dejá que la librería valide
state. Authlib, Passport (state: true) y Spring Security lo hacen solos; solo no los puentees con callbacks manuales. - Pedí scopes mínimos.
openid email profilealcanza para el login; agregá más solo cuando una feature lo necesite. - Guardá los tokens del lado del servidor. Mantené access y refresh tokens fuera de
localStorage; un payload de XSS no debería poder llevárselos. - Normalizá los perfiles de proveedor. Guardá
provider+provider_user_idy mapeá email/nombre a una sola forma, así sumar GitHub después es trabajo de datos, no de auth.
Errores Comunes
- Saltearte la validación de
state. Armar tu propio handler de callback y olvidarte delstateabre la puerta al login CSRF. - Tokens en
localStorage. Cualquier script inyectado los puede leer. Cookies HttpOnly o sesiones server-side son el lugar seguro. redirect_urique no coincide. Los proveedores comparan byte a byte: una barra final o un puerto distinto rompe el callback. Registrá la URI exacta.- Pedir scopes amplios de entrada. Los usuarios abandonan pantallas de consentimiento que piden más de lo que el login necesita.
- Tratar OAuth como autenticación. OAuth es autorización; el
id_tokeny el/userinfovienen de la capa OpenID Connect encima. Confundir las dos lleva a aceptar tokens destinados a otro.
Checklist de Seguridad
Los esenciales específicos de OAuth, condensados del RFC 9700:
-
stategenerado por petición y validado en el callback - PKCE obligatorio; no dejes que el proveedor degrade el flujo
-
redirect_uriregistrada y matcheada exacta, nunca por prefijo - Refresh tokens rotados en cada uso donde el proveedor lo soporte
- Scopes verificados por endpoint: un token de
readno debería alcanzar endpoints dewrite - Sin tokens en URLs, query params o fragments (se filtran por logs y Referer)
- Tokens nunca en
localStorage(XSS los roba) - HTTPS en todo el flujo, nada de mixed content
- Refresh tokens invalidados al hacer logout
- Flujo Implicit deshabilitado (está deprecado)
Solución de Problemas
invalid_stateen el callback. Elstatedel callback no coincide con el de la sesión, suele ser una cookie de sesión perdida (revisáSameSite) o el usuario empezó el login en una pestaña y terminó en otra.redirect_uri_mismatch. La URI enviada no coincide byte a byte con la registrada. Compará protocolo, host, puerto y barra final.- La pantalla de consentimiento muestra
access_denied. El usuario rechazó, o tu app no está verificada para un scope sensible. Compará la lista de scopes con los requisitos de verificación de Google. - El intercambio de token devuelve
invalid_grant. El código expiró (viven ~10 minutos), ya se usó, o elredirect_uridel intercambio no coincide con el del paso authorize. - Loop de login. El callback funciona pero la cookie de sesión no se setea — revisá
Secure,SameSitey si hay un proxy que está quitando elSet-Cookie.
Preguntas frecuentes
¿Puedo usar OAuth 2.0 para autenticación máquina a máquina?
Sí, con el flujo Client Credentials: el servicio se autentica con su propio client ID y secret, sin usuario de por medio. Para APIs internas más simples, las API keys pueden alcanzar.
¿Cómo soporto varios proveedores (Google, GitHub, Microsoft)?
Usá una librería que abstraiga las diferencias (Authlib, Passport, Spring Security). Guardá provider y provider_user_id en tu tabla de usuarios y normalizá email/nombre a una sola forma de perfil. Cada proveedor documenta sus propias particularidades — el flujo OAuth de GitHub, por ejemplo, difiere del de Google en formato de scopes y tipos de token. Para un walkthrough específico de GitHub en Node.js, mirá Node.js OAuth2 GitHub Login.
¿Cuál es la diferencia entre OAuth 2.0 y OpenID Connect?
OAuth 2.0 es un framework de autorización ("¿puede esta app acceder a mis datos?"). OpenID Connect es una capa de autenticación encima que estandariza los claims de identidad vía el id_token y el /userinfo. Si solo necesitás saber quién es el usuario, OIDC alcanza; si necesitás acceso a APIs en su nombre, hacen falta los scopes de OAuth también.
¿De verdad necesito PKCE para una app server-side?
El RFC 9700 lo recomienda para todos los clientes, confidenciales o no. Cuesta un flag y mata los ataques de intercepción de código aunque la respuesta de autorización se filtre por logs o extensiones del navegador. No hay buena razón para dejarlo apagado.
¿Dónde debería guardar los tokens?
Del lado del servidor, atados a la sesión del usuario. El localStorage del navegador y las cookies sin HttpOnly exponen los tokens a cualquier script que caiga en la página. Si el proveedor ofrece rotación de refresh tokens, activala — un token robado muere después del primer uso.
¿Por qué está deprecado el flujo Implicit?
Devolvía access tokens en el fragmento de la URL, donde extensiones del navegador, el historial y los logs podían capturarlos, y no podía usar PKCE ni refresh tokens. Authorization Code + PKCE lo reemplazó; mirá las notas de migración de oauth.net.
Código companion: El repo companion de oauth2-login tiene los tres ejemplos como apps completas ejecutables, más el application.yml de Spring para Google y GitHub.
Recursos Relacionados
Autenticación JWT
Firma, verifica y refresca JWTs en Python, JavaScript y Java — flujos access/refresh, pares de claves RS256 y revocación de tokens bien hecha.
RecipeLogin con OAuth2 de GitHub en Node.js con Express
Implementa el flujo de login OAuth2 de GitHub en Node.js con Express y Passport.
RecipeImplementar Login Sin Contraseña con Magic Links
Cómo construir autenticación passwordless segura usando links mágicos de tiempo limitado enviados por email, con generación de tokens, validación y prevención de ataques replay.
RecipeAutenticación Segura con API Keys para Servicios y Clientes
Cómo generar, distribuir, validar y rotar API keys para autenticación machine-to-machine usando firmas HMAC, scopes y políticas de rate limiting.
RecipeImplementar Gestión de Sesiones Segura
Cómo crear, validar y expirar sesiones de usuario de forma segura usando cookies HTTP-only, almacenamiento en Redis y protección CSRF.
RecipeAutenticación de Dos Factores (2FA / TOTP)
Cómo implementar autenticación de dos factores con contraseñas de un solo uso basadas en tiempo (TOTP) para login seguro.