🔐 ¿Qué es OAuth 2.0?
📖 Definición
OAuth 2.0 es un protocolo estándar abierto que permite a los usuarios otorgar acceso a su información a otros sitios web o aplicaciones sin compartir su contraseña. Ejemplos representativos son el inicio de sesión social como "Iniciar sesión con Google" o "Iniciar sesión con GitHub". Se centra en la autorización (Authorization) en lugar de la autenticación (Authentication), proporcionando una experiencia de usuario segura y conveniente.
🎯 Entenderlo con analogías
Tarjeta llave de hotel
Método tradicional = Llave maestra del hotel
Dueño del hotel: "Le daré la llave maestra"
Tú: "¡Pero esta abre todas las habitaciones! 😱"
"Es como dar mi contraseña"
OAuth 2.0 = Tarjeta llave de hotel
Dueño del hotel: "Esta tarjeta llave..."
✅ Solo abre tu habitación
✅ Expira automáticamente al hacer checkout
✅ Si la pierdes, la llave maestra sigue segura
✅ Puedes invalidarla cuando quieras
Tú: "¡Perfecto!"
Poder notarial
Escenario: Delegar asuntos bancarios a un representante
❌ Método de compartir contraseña
Tú: "Mi contraseña bancaria es 1234"
Representante: "¡Puedo hacer todo en el banco!"
Riesgo: Todos los permisos como retirar fondos, eliminar cuentas, etc.
✅ Método OAuth
Tú: "Voy al banco y hago un poder notarial"
Banco: "¿Qué permisos quieres otorgar?"
Tú: "Solo consultar el saldo"
Banco: "¿Por cuánto tiempo?"
Tú: "Solo una semana"
El representante puede:
✅ Solo consultar el saldo
✅ Expira automáticamente después de una semana
✅ No conoce tu contraseña
✅ Puedes revocar el permiso en cualquier momento
⚙️ Cómo funciona
1. Conceptos clave de OAuth 2.0
┌─────────────────────────────────────────┐
│ 4 roles de OAuth 2.0 │
└─────────────────────────────────────────┘
1. Resource Owner (Propietario del recurso)
└─ Usuario (tú)
2. Client (Cliente)
└─ Aplicación que desea usar el servicio
└─ Ejemplo: tu sitio web
3. Authorization Server (Servidor de autorización)
└─ Servidor que otorga permisos
└─ Ejemplo: Google, GitHub, Naver
4. Resource Server (Servidor de recursos)
└─ Servidor que contiene la información del usuario
└─ Ejemplo: Google API, GitHub API
2. Flujo de autenticación OAuth 2.0 (Authorization Code)
┌──────────┐ ┌──────────┐
│ Usuario │ │ Cliente │
│ (tú) │ │ (webapp) │
└─────┬────┘ └────┬─────┘
│ │
│ 1. Clic en "Iniciar sesión con Google" │
│──────────────────────────────────────────>│
│ │
│ 2. Redirigir a página de inicio Google │
│<──────────────────────────────────────────│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ 3. Login│ (Auth Server) │ │
│────────>│ │ │
│ │ │ │
│ 4. Solicitud de permiso │
│ "Esta app quiere ver tu │
│ email y perfil" │
│<────────│ │ │
│ │ │ │
│ 5. Aprobar │
│────────>│ │ │
│ │ │ │
│ │ 6. Emitir Authorization Code │
│ │ (código de un solo uso) │
│<────────│ │ │
│ └──────────────────┘ │
│ │
│ 7. Enviar Authorization Code │
│──────────────────────────────────────────>│
│ │
│ ┌──────────────────┐ │
│ │ Google │ │
│ │ (Auth Server) │ 8. Intercambio│
│ │ │<─── Code + │
│ │ │ Client │
│ │ │ Secret │
│ │ │ │
│ │ 9. Emitir Access Token ────────>│
│ │ │ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────┐ │
│ │ Google API │ │
│ │ (Resource │ 10. Solicitar│
│ │ Server) │<──── info con │
│ │ │ Token │
│ │ │ │
│ │ 11. Respuesta con info ────────>│
│ │ del usuario │ │
│ └──────────────────┘ │
│ │
│ 12. ¡Inicio de sesión completo! │
│<──────────────────────────────────────────│
│ │
Puntos clave:
- La contraseña del usuario solo se ingresa en Google
- La webapp nunca puede conocer la contraseña
- Solo accede a información autorizada con Access Token
3. Tipos de Grant de OAuth 2.0
========== 1. Authorization Code (más seguro) ==========
Uso: Aplicaciones web del lado del servidor
Ventajas: Más seguro (usa Client Secret)
Ejemplo: "Iniciar sesión con Google"
Flujo:
1. Usuario → Inicio de sesión Google
2. Google → Emite Authorization Code
3. Servidor → Intercambia Code + Secret por Access Token
4. Servidor → Llama API con Token
========== 2. Implicit (simple pero menos seguro) ==========
Uso: Apps solo de navegador (SPA)
Ventajas: Simple (no requiere servidor)
Desventajas: Vulnerable (Token expuesto en URL)
Ejemplo: SPA antiguas
Flujo:
1. Usuario → Inicio de sesión Google
2. Google → Emite Access Token directamente (en URL)
3. Navegador → Llama API con Token
⚠️ Actualmente no recomendado
========== 3. Resource Owner Password (legacy) ==========
Uso: Solo apps de confianza
Ventajas: Simple
Desventajas: La app conoce la contraseña del usuario
Ejemplo: App móvil propia
Flujo:
1. App → Recibe ID/contraseña del usuario
2. App → Envía a Authorization Server
3. Authorization Server → Emite Access Token
⚠️ Contradice la filosofía OAuth, no recomendado
========== 4. Client Credentials ==========
Uso: Comunicación entre servidores (M2M)
Ventajas: No requiere autenticación de usuario
Ejemplo: Comunicación entre servicios backend
Flujo:
1. Servidor → Envía Client ID + Secret
2. Authorization Server → Emite Access Token
3. Servidor → Llama API con Token
========== 5. PKCE (Proof Key for Code Exchange) ==========
Uso: Apps móviles, SPA (recomendado actualmente)
Ventajas: Authorization Code + seguridad adicional
Ejemplo: Apps móviles/SPA modernas
Flujo:
1. App → Genera code_verifier (cadena aleatoria)
2. App → Genera code_challenge (hash del verifier)
3. App → Solicita Authorization Code con challenge
4. Google → Emite Code
5. App → Intercambia Code + verifier por Token
6. Google → Verifica verifier y emite Token
💡 Ejemplos reales
Inicio de sesión Google OAuth 2.0 (Node.js)
// ========== 1. Configuración de Google Cloud Console ==========
/*
1. Acceder a https://console.cloud.google.com
2. Crear proyecto
3. "APIs y servicios" → "Credenciales"
4. Crear "ID de cliente de OAuth 2.0"
5. Agregar URI de redireccionamiento autorizado:
http://localhost:3000/auth/google/callback
6. Obtener Client ID y Client Secret
*/
// ========== 2. Implementación del servidor ==========
const express = require('express');
const axios = require('axios');
const session = require('express-session');
const app = express();
// Configuración de sesión
app.use(session({
secret: 'your-session-secret',
resave: false,
saveUninitialized: true
}));
// Configuración de Google OAuth
const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID;
const GOOGLE_CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/google/callback';
// ========== Paso 1: Iniciar sesión ==========
app.get('/auth/google', (req, res) => {
// Generar URL de autenticación de Google
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code', // Solicitar Authorization Code
scope: 'openid email profile', // Permisos a solicitar
access_type: 'offline', // Recibir Refresh Token
prompt: 'consent' // Siempre mostrar pantalla de consentimiento
});
// Redirigir a página de inicio de sesión de Google
res.redirect(`${authUrl}?${params}`);
});
// ========== Paso 2: Manejar callback ==========
app.get('/auth/google/callback', async (req, res) => {
const { code, error } = req.query;
// Si el usuario rechaza
if (error) {
return res.status(400).send(`Autenticación fallida: ${error}`);
}
try {
// Intercambiar Authorization Code por Access Token
const tokenResponse = await axios.post(
'https://oauth2.googleapis.com/token',
{
code,
client_id: GOOGLE_CLIENT_ID,
client_secret: GOOGLE_CLIENT_SECRET,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code'
}
);
const {
access_token,
refresh_token,
expires_in,
token_type
} = tokenResponse.data;
console.log('Access Token:', access_token);
console.log('Expira en:', expires_in, 'segundos');
// Obtener información del usuario con Access Token
const userResponse = await axios.get(
'https://www.googleapis.com/oauth2/v2/userinfo',
{
headers: {
Authorization: `Bearer ${access_token}`
}
}
);
const user = userResponse.data;
/*
{
id: "123456789",
email: "user@gmail.com",
verified_email: true,
name: "Juan García",
picture: "https://lh3.googleusercontent.com/...",
locale: "es"
}
*/
// Guardar información del usuario en sesión
req.session.user = {
id: user.id,
email: user.email,
name: user.name,
picture: user.picture,
accessToken: access_token,
refreshToken: refresh_token
};
// Guardar usuario en base de datos
await saveOrUpdateUser(user);
// Redirigir a página principal
res.redirect('/dashboard');
} catch (error) {
console.error('Error OAuth:', error.response?.data || error.message);
res.status(500).send('Error al procesar autenticación');
}
});
// ========== Verificar información del usuario ==========
app.get('/dashboard', (req, res) => {
if (!req.session.user) {
return res.redirect('/');
}
res.send(`
<h1>¡Hola, ${req.session.user.name}!</h1>
<img src="${req.session.user.picture}" alt="Perfil" />
<p>Email: ${req.session.user.email}</p>
<a href="/logout">Cerrar sesión</a>
`);
});
// ========== Cerrar sesión ==========
app.get('/logout', async (req, res) => {
// Revocar Access Token de Google (opcional)
const accessToken = req.session.user?.accessToken;
if (accessToken) {
try {
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
} catch (error) {
console.error('Error al revocar token:', error.message);
}
}
// Eliminar sesión
req.session.destroy();
res.redirect('/');
});
// ========== Página principal ==========
app.get('/', (req, res) => {
res.send(`
<h1>Demo OAuth 2.0</h1>
<a href="/auth/google">
<button>Iniciar sesión con Google</button>
</a>
`);
});
app.listen(3000, () => {
console.log('Servidor ejecutándose: http://localhost:3000');
});
Implementación fácil con Passport.js
// ========== Uso de Passport.js ==========
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
// Configuración de Passport
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: 'http://localhost:3000/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
// Procesar información del usuario
try {
// Buscar o crear usuario en base de datos
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
picture: profile.photos[0].value
});
}
done(null, user);
} catch (error) {
done(error, null);
}
}
));
// Serialización de sesión
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
const user = await User.findById(id);
done(null, user);
});
// Configuración de Express
app.use(passport.initialize());
app.use(passport.session());
// ========== Rutas ==========
// Iniciar inicio de sesión Google
app.get('/auth/google',
passport.authenticate('google', {
scope: ['profile', 'email']
})
);
// Callback de Google
app.get('/auth/google/callback',
passport.authenticate('google', {
failureRedirect: '/login',
successRedirect: '/dashboard'
})
);
// Cerrar sesión
app.get('/logout', (req, res) => {
req.logout((err) => {
if (err) console.error(err);
res.redirect('/');
});
});
// Middleware de autenticación
function isAuthenticated(req, res, next) {
if (req.isAuthenticated()) {
return next();
}
res.redirect('/login');
}
// Ruta protegida
app.get('/dashboard', isAuthenticated, (req, res) => {
res.send(`¡Hola, ${req.user.name}!`);
});
GitHub OAuth 2.0
// ========== Configuración de GitHub OAuth ==========
/*
1. Acceder a https://github.com/settings/developers
2. Clic en "New OAuth App"
3. Ingresar información:
- Application name: My App
- Homepage URL: http://localhost:3000
- Authorization callback URL: http://localhost:3000/auth/github/callback
4. Obtener Client ID y Client Secret
*/
const express = require('express');
const axios = require('axios');
const app = express();
const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID;
const GITHUB_CLIENT_SECRET = process.env.GITHUB_CLIENT_SECRET;
const REDIRECT_URI = 'http://localhost:3000/auth/github/callback';
// ========== Inicio de sesión GitHub ==========
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email', // Permisos a solicitar
state: Math.random().toString(36) // Prevención CSRF
});
res.redirect(`${authUrl}?${params}`);
});
// ========== Manejar callback ==========
app.get('/auth/github/callback', async (req, res) => {
const { code, state } = req.query;
try {
// 1. Obtener Access Token
const tokenResponse = await axios.post(
'https://github.com/login/oauth/access_token',
{
client_id: GITHUB_CLIENT_ID,
client_secret: GITHUB_CLIENT_SECRET,
code,
redirect_uri: REDIRECT_URI
},
{
headers: { Accept: 'application/json' }
}
);
const { access_token } = tokenResponse.data;
// 2. Obtener información del usuario
const userResponse = await axios.get('https://api.github.com/user', {
headers: {
Authorization: `Bearer ${access_token}`
}
});
const user = userResponse.data;
/*
{
login: "username",
id: 12345,
avatar_url: "https://avatars.githubusercontent.com/...",
name: "Juan García",
email: "user@example.com",
bio: "Soy desarrollador",
public_repos: 50
}
*/
// 3. Obtener email (solicitud separada)
const emailResponse = await axios.get('https://api.github.com/user/emails', {
headers: {
Authorization: `Bearer ${access_token}`
}
});
const primaryEmail = emailResponse.data.find(email => email.primary);
res.json({
user,
email: primaryEmail.email
});
} catch (error) {
console.error('Error OAuth GitHub:', error.message);
res.status(500).send('Autenticación fallida');
}
});
app.listen(3000);
Usar OAuth en React
// ========== Cliente React ==========
import React, { useEffect, useState } from 'react';
import axios from 'axios';
function App() {
const [user, setUser] = useState(null);
// Verificar información del usuario al cargar la página
useEffect(() => {
checkAuth();
}, []);
async function checkAuth() {
try {
const response = await axios.get('/api/me', {
withCredentials: true // Incluir cookies
});
setUser(response.data);
} catch (error) {
console.log('No ha iniciado sesión');
}
}
function handleGoogleLogin() {
// Ir al endpoint OAuth del servidor
window.location.href = 'http://localhost:3000/auth/google';
}
function handleGitHubLogin() {
window.location.href = 'http://localhost:3000/auth/github';
}
function handleLogout() {
window.location.href = 'http://localhost:3000/logout';
}
if (user) {
return (
<div>
<h1>¡Hola, {user.name}!</h1>
<img src={user.picture} alt="Perfil" />
<p>{user.email}</p>
<button onClick={handleLogout}>Cerrar sesión</button>
</div>
);
}
return (
<div>
<h1>Iniciar sesión</h1>
<button onClick={handleGoogleLogin}>
🔵 Iniciar sesión con Google
</button>
<button onClick={handleGitHubLogin}>
⚫ Iniciar sesión con GitHub
</button>
</div>
);
}
export default App;
Mejorar seguridad con PKCE (SPA/Móvil)
// ========== PKCE (Proof Key for Code Exchange) ==========
// Para usar en apps móviles o SPA
const crypto = require('crypto');
// ========== 1. Generar Code Verifier ==========
function generateCodeVerifier() {
return base64URLEncode(crypto.randomBytes(32));
}
// ========== 2. Generar Code Challenge ==========
function generateCodeChallenge(verifier) {
const hash = crypto.createHash('sha256')
.update(verifier)
.digest();
return base64URLEncode(hash);
}
function base64URLEncode(buffer) {
return buffer.toString('base64')
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
}
// ========== 3. Iniciar inicio de sesión PKCE (cliente) ==========
async function startPKCELogin() {
// Generar y guardar Verifier
const codeVerifier = generateCodeVerifier();
sessionStorage.setItem('code_verifier', codeVerifier);
// Generar Challenge
const codeChallenge = generateCodeChallenge(codeVerifier);
// URL de autenticación de Google
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'openid email profile',
code_challenge: codeChallenge, // PKCE
code_challenge_method: 'S256' // SHA-256
});
window.location.href = `${authUrl}?${params}`;
}
// ========== 4. Manejar callback ==========
async function handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
// Obtener Verifier guardado
const codeVerifier = sessionStorage.getItem('code_verifier');
// Intercambiar Token
const response = await fetch('https://oauth2.googleapis.com/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code,
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // PKCE Verifier
})
});
const { access_token } = await response.json();
// Eliminar Verifier
sessionStorage.removeItem('code_verifier');
return access_token;
}
🤔 Preguntas frecuentes
P1. ¿OAuth vs JWT vs Sesión?
R:
// ========== 1. Sesión (Session) ==========
// Método tradicional
// Inicio de sesión
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Verificar autenticación
if (isValidUser(username, password)) {
// Guardar en sesión
req.session.userId = user.id;
res.json({ message: 'Inicio de sesión exitoso' });
}
});
// Verificar autenticación
app.get('/profile', (req, res) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Inicio de sesión requerido' });
}
const user = getUserById(req.session.userId);
res.json(user);
});
Ventajas: Simple, control total del servidor
Desventajas: Requiere memoria/almacenamiento del servidor, baja escalabilidad
// ========== 2. JWT (JSON Web Token) ==========
// Autenticación Stateless
// Inicio de sesión
app.post('/login', (req, res) => {
const { username, password } = req.body;
if (isValidUser(username, password)) {
// Generar JWT
const token = jwt.sign(
{ userId: user.id, username: user.username },
SECRET_KEY,
{ expiresIn: '1h' }
);
res.json({ token });
}
});
// Verificar autenticación
app.get('/profile', (req, res) => {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET_KEY);
const user = getUserById(decoded.userId);
res.json(user);
} catch (error) {
res.status(401).json({ error: 'Token inválido' });
}
});
Ventajas: Stateless, buena escalabilidad
Desventajas: Difícil de invalidar, tamaño grande
// ========== 3. OAuth 2.0 ==========
// Autenticación y autorización de terceros
// Usuario inicia sesión con Google
// → Google emite Access Token
// → Nuestra app llama a API de Google con Token
Ventajas:
- No comparte contraseña
- Control granular de permisos
- Protocolo estándar
- Inicio de sesión social posible
Desventajas:
- Complejo
- Dependencia de servicio externo
// ========== Comparación ==========
Característica | Sesión | JWT | OAuth
--------------|---------|----------|-------
Ubicación | Servidor| Cliente | Servidor
Escalabilidad | Baja | Alta | Alta
Seguridad | Alta | Media | Alta
Complejidad | Baja | Media | Alta
Auth terceros | No | No | Sí
// ========== Uso combinado ==========
// Emitir JWT después de iniciar sesión con OAuth
app.get('/auth/google/callback', async (req, res) => {
// 1. Autenticar usuario con Google OAuth
const googleUser = await getGoogleUser(req.query.code);
// 2. Guardar usuario en nuestra base de datos
const user = await saveOrUpdateUser(googleUser);
// 3. Generar y devolver JWT
const token = jwt.sign(
{ userId: user.id, email: user.email },
SECRET_KEY,
{ expiresIn: '7d' }
);
res.json({ token });
});
// Las solicitudes posteriores se autentican con JWT
app.get('/api/profile', authenticateJWT, (req, res) => {
res.json(req.user);
});
P2. ¿Cuál es la diferencia entre Access Token y Refresh Token?
R:
// ========== Access Token ==========
// Vida corta (15 minutos~1 hora)
// Se usa en solicitudes API
// ========== Refresh Token ==========
// Vida larga (7 días~30 días)
// Se usa para renovar Access Token
// ========== Ejemplo de implementación ==========
// Emitir ambos al iniciar sesión
app.post('/login', async (req, res) => {
const user = await authenticateUser(req.body);
// Access Token (15 minutos)
const accessToken = jwt.sign(
{ userId: user.id, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
// Refresh Token (7 días)
const refreshToken = jwt.sign(
{ userId: user.id, type: 'refresh' },
REFRESH_SECRET,
{ expiresIn: '7d' }
);
// Guardar Refresh Token en DB (para invalidación)
await saveRefreshToken(user.id, refreshToken);
res.json({ accessToken, refreshToken });
});
// ========== Solicitud API (Access Token) ==========
app.get('/api/data', authenticateAccess, (req, res) => {
res.json({ data: 'Datos importantes' });
});
function authenticateAccess(req, res, next) {
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, ACCESS_SECRET);
if (decoded.type !== 'access') {
throw new Error('Tipo de token incorrecto');
}
req.user = decoded;
next();
} catch (error) {
if (error.name === 'TokenExpiredError') {
return res.status(401).json({
error: 'access_token_expired',
message: 'Renovar con Refresh Token'
});
}
res.status(401).json({ error: 'Token inválido' });
}
}
// ========== Renovar Access Token ==========
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
if (!refreshToken) {
return res.status(401).json({ error: 'Refresh Token requerido' });
}
try {
// 1. Verificar Refresh Token
const decoded = jwt.verify(refreshToken, REFRESH_SECRET);
if (decoded.type !== 'refresh') {
throw new Error('Tipo de token incorrecto');
}
// 2. Verificar en DB (si está invalidado)
const isValid = await isRefreshTokenValid(decoded.userId, refreshToken);
if (!isValid) {
throw new Error('Refresh Token invalidado');
}
// 3. Emitir nuevo Access Token
const newAccessToken = jwt.sign(
{ userId: decoded.userId, type: 'access' },
ACCESS_SECRET,
{ expiresIn: '15m' }
);
res.json({ accessToken: newAccessToken });
} catch (error) {
res.status(401).json({ error: 'Refresh Token inválido' });
}
});
// ========== Renovación automática en frontend ==========
import axios from 'axios';
let accessToken = localStorage.getItem('accessToken');
let refreshToken = localStorage.getItem('refreshToken');
// Interceptor de Axios
axios.interceptors.request.use(
config => {
config.headers.Authorization = `Bearer ${accessToken}`;
return config;
}
);
axios.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config;
// Access Token expirado
if (error.response?.data?.error === 'access_token_expired' &&
!originalRequest._retry) {
originalRequest._retry = true;
try {
// Renovar con Refresh Token
const response = await axios.post('/auth/refresh', {
refreshToken
});
accessToken = response.data.accessToken;
localStorage.setItem('accessToken', accessToken);
// Reintentar solicitud original
originalRequest.headers.Authorization = `Bearer ${accessToken}`;
return axios(originalRequest);
} catch (refreshError) {
// Refresh Token también expirado → Cerrar sesión
localStorage.clear();
window.location.href = '/login';
}
}
return Promise.reject(error);
}
);
// ========== Consejos de seguridad ==========
// ✅ Buena práctica
1. Access Token: Vida corta (15 minutos)
2. Refresh Token: Guardar en DB para invalidación
3. Refresh Token en cookie HttpOnly
4. HTTPS obligatorio
5. Refresh Token Rotation (emitir nuevo token al renovar)
// ❌ Mala práctica
1. Vida de Access Token demasiado larga
2. Guardar Refresh Token en localStorage
3. Refresh Token no invalidable
4. Usar HTTP
P3. ¿Qué es Scope (alcance de permisos)?
R:
// ========== Concepto de Scope ==========
// Alcance de los permisos que solicita la aplicación
// ========== Scopes de Google OAuth ==========
const scopes = [
'openid', // Información básica
'email', // Email
'profile', // Perfil (nombre, foto)
'https://www.googleapis.com/auth/drive.readonly', // Lectura Drive
'https://www.googleapis.com/auth/gmail.send' // Envío Gmail
];
// Solicitar permisos al iniciar sesión
app.get('/auth/google', (req, res) => {
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: scopes.join(' ') // Separar con espacio
});
res.redirect(`${authUrl}?${params}`);
});
// Pantalla mostrada al usuario:
/*
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
My App solicita:
✓ Ver dirección de email
✓ Ver información básica del perfil
✓ Ver archivos de Google Drive
✓ Enviar emails de Gmail
[Permitir] [Denegar]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
*/
// ========== Scopes de GitHub OAuth ==========
const githubScopes = [
'user', // Información del usuario
'user:email', // Email (incluye privado)
'repo', // Acceso completo a repositorios
'public_repo', // Solo repositorios públicos
'read:org', // Leer información de organización
'write:org' // Escribir información de organización
];
app.get('/auth/github', (req, res) => {
const authUrl = 'https://github.com/login/oauth/authorize';
const params = new URLSearchParams({
client_id: GITHUB_CLIENT_ID,
redirect_uri: REDIRECT_URI,
scope: 'user:email public_repo' // Solo permisos necesarios
});
res.redirect(`${authUrl}?${params}`);
});
// ========== Principio de mínimo privilegio ==========
// ❌ Mala práctica: Solicitar más permisos de los necesarios
scope: 'user repo admin:org' // ¡Demasiados permisos!
// ✅ Buena práctica: Solo permisos necesarios
scope: 'user:email' // Solo email necesario
// ========== Solicitud dinámica de Scope ==========
// Inicio de sesión inicial: Solo información básica
app.get('/auth/google', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile' // Solo información básica
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// Cuando se necesiten permisos adicionales más tarde
app.get('/auth/google/drive', (req, res) => {
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'https://www.googleapis.com/auth/drive.readonly',
prompt: 'consent' // Solicitar consentimiento nuevamente
});
res.redirect(`https://accounts.google.com/o/oauth2/v2/auth?${params}`);
});
// ========== Autorización incremental ==========
// Agregar permisos gradualmente cuando sea necesario
// Paso 1: Inicio de sesión (información básica)
scope: 'email profile'
// Paso 2: Integración Google Drive (cuando el usuario lo desee)
scope: 'https://www.googleapis.com/auth/drive'
// Paso 3: Integración Gmail (cuando el usuario lo desee)
scope: 'https://www.googleapis.com/auth/gmail.send'
// ========== Verificación de Scope ==========
// Verificar scope incluido en Access Token
async function checkScope(accessToken, requiredScope) {
const response = await axios.get(
'https://www.googleapis.com/oauth2/v1/tokeninfo',
{ params: { access_token: accessToken } }
);
const grantedScopes = response.data.scope.split(' ');
if (!grantedScopes.includes(requiredScope)) {
throw new Error('Permisos insuficientes');
}
}
// Ejemplo de uso
app.get('/api/drive/files', async (req, res) => {
const accessToken = req.headers.authorization?.split(' ')[1];
try {
// Verificar permiso de Drive
await checkScope(accessToken, 'https://www.googleapis.com/auth/drive.readonly');
// Llamar API de Drive
const files = await listDriveFiles(accessToken);
res.json(files);
} catch (error) {
res.status(403).json({
error: 'Permisos insuficientes',
message: 'Se requiere acceso a Google Drive'
});
}
});
P4. ¿Cuáles son las mejores prácticas de seguridad en OAuth?
R:
// ========== 1. HTTPS obligatorio ==========
// ✅ Siempre usar HTTPS
const REDIRECT_URI = 'https://myapp.com/callback';
// ❌ HTTP absolutamente prohibido
const REDIRECT_URI = 'http://myapp.com/callback'; // ¡Riesgo de robo de token!
// ========== 2. Parámetro State (Prevención CSRF) ==========
// Iniciar sesión
app.get('/auth/google', (req, res) => {
// Generar y guardar state aleatorio
const state = crypto.randomBytes(16).toString('hex');
req.session.oauthState = state;
const authUrl = 'https://accounts.google.com/o/oauth2/v2/auth';
const params = new URLSearchParams({
client_id: GOOGLE_CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
scope: 'email profile',
state // Prevención CSRF
});
res.redirect(`${authUrl}?${params}`);
});
// Verificar en callback
app.get('/auth/google/callback', (req, res) => {
const { state, code } = req.query;
// Verificar state
if (state !== req.session.oauthState) {
return res.status(403).send('Ataque CSRF detectado');
}
// Eliminar state
delete req.session.oauthState;
// ... continuar procesamiento
});
// ========== 3. Proteger Client Secret ==========
// ✅ Usar variables de entorno
const CLIENT_SECRET = process.env.GOOGLE_CLIENT_SECRET;
// ✅ Usar solo en el servidor
// ¡Nunca exponer en frontend!
// ❌ No hardcodear en código
const CLIENT_SECRET = 'abc123xyz'; // ¡Absolutamente prohibido!
// ========== 4. Validar Redirect URI ==========
// Solo permitir URIs registradas en Google Cloud Console
// ✅ URI que coincide exactamente
https://myapp.com/auth/callback
// ❌ No usar wildcards
https://myapp.com/*
// ❌ Vulnerabilidad de redirección abierta
https://myapp.com/callback?redirect=evil.com
// ========== 5. Almacenamiento de Token ==========
// ✅ Servidor: Variables de entorno o DB cifrada
await saveToken(userId, encryptToken(accessToken));
// ✅ Cliente: Cookie HttpOnly (Prevención XSS)
res.cookie('access_token', accessToken, {
httpOnly: true,
secure: true, // Solo HTTPS
sameSite: 'strict',
maxAge: 3600000 // 1 hora
});
// ❌ localStorage (vulnerable a XSS)
localStorage.setItem('token', accessToken); // ¡Peligroso!
// ========== 6. Tiempo de expiración del Token ==========
// ✅ Vida corta
const accessToken = jwt.sign(payload, secret, {
expiresIn: '15m' // 15 minutos
});
// ✅ Usar Refresh Token
const refreshToken = jwt.sign(payload, secret, {
expiresIn: '7d' // 7 días
});
// ❌ Sin tiempo de expiración
const token = jwt.sign(payload, secret); // ¡Peligroso!
// ========== 7. Minimizar Scope ==========
// ✅ Solo permisos necesarios
scope: 'email profile'
// ❌ Solicitar todos los permisos
scope: 'https://www.googleapis.com/auth/drive' // Innecesario
// ========== 8. Implementar revocación de Token ==========
// Revocar Token al cerrar sesión
app.get('/logout', async (req, res) => {
const accessToken = req.cookies.access_token;
// Revocar Token de Google
await axios.post(
`https://oauth2.googleapis.com/revoke?token=${accessToken}`
);
// Eliminar Refresh Token de DB
await deleteRefreshToken(req.user.id);
// Eliminar cookie
res.clearCookie('access_token');
res.redirect('/');
});
// ========== 9. Usar PKCE (Móvil/SPA) ==========
// Mejorar seguridad sin Client Secret
const codeVerifier = generateRandomString(128);
const codeChallenge = base64URLEncode(sha256(codeVerifier));
// Enviar challenge en solicitud de autenticación
const params = {
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
response_type: 'code',
code_challenge: codeChallenge,
code_challenge_method: 'S256'
};
// Enviar verifier al intercambiar Token
const tokenParams = {
code,
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
grant_type: 'authorization_code',
code_verifier: codeVerifier // En lugar de Secret
};
// ========== 10. Renovación regular de Token ==========
// Renovación automática en segundo plano
setInterval(async () => {
try {
const newAccessToken = await refreshAccessToken(refreshToken);
updateToken(newAccessToken);
} catch (error) {
console.error('Error al renovar token:', error);
redirectToLogin();
}
}, 14 * 60 * 1000); // Cada 14 minutos (antes de expirar a los 15)
P5. ¿Autenticación (Authentication) vs Autorización (Authorization)?
R:
// ========== Autenticación (Authentication) ==========
// "¿Quién eres?" - Verificación de identidad
// Ejemplo: Inicio de sesión
const user = await authenticateUser(username, password);
if (user) {
console.log('Eres Juan García ✓');
}
// ========== Autorización (Authorization) ==========
// "¿Qué puedes hacer?" - Verificación de permisos
// Ejemplo: Solo acceso para administrador
if (user.role === 'admin') {
console.log('Funciones de administrador disponibles ✓');
} else {
console.log('Sin permisos ✗');
}
// ========== Rol de OAuth 2.0 ==========
// OAuth es principalmente Authorization (autorización)
// - Usuario otorga permisos a la app
// - "Permitir que esta app vea mi Google Drive"
// Pero también se usa para autenticación (Authentication)
// - OpenID Connect (OAuth 2.0 + autenticación)
// - "Iniciar sesión con Google"
// ========== Ejemplo real ==========
// Paso 1: Autenticación (Authentication)
app.post('/login', async (req, res) => {
const { username, password } = req.body;
// Verificar identidad del usuario
const user = await User.findOne({ username });
if (!user || !await bcrypt.compare(password, user.password)) {
return res.status(401).json({ error: 'Autenticación fallida' });
}
// Autenticación exitosa → Emitir token
const token = jwt.sign(
{
userId: user.id,
role: user.role // Incluir información de permisos
},
SECRET_KEY
);
res.json({ token });
});
// Paso 2: Autorización (Authorization)
app.get('/admin/users', authenticateToken, authorizeAdmin, (req, res) => {
// Solo administradores pueden acceder
const users = await User.findAll();
res.json(users);
});
function authenticateToken(req, res, next) {
// Autenticación: Verificar token
const token = req.headers.authorization?.split(' ')[1];
try {
const decoded = jwt.verify(token, SECRET_KEY);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({ error: 'Autenticación requerida' });
}
}
function authorizeAdmin(req, res, next) {
// Autorización: Verificar administrador
if (req.user.role !== 'admin') {
return res.status(403).json({ error: 'Sin permisos' });
}
next();
}
// ========== Autenticación + Autorización con OAuth ==========
// 1. Autenticación (OpenID Connect)
app.get('/auth/google/callback', async (req, res) => {
// Verificar usuario con Google OAuth
const googleUser = await getGoogleUser(req.query.code);
// Autenticación completada → Identidad verificada
console.log('Usuario:', googleUser.email);
// Iniciar sesión en nuestro sistema
const user = await findOrCreateUser(googleUser);
req.session.userId = user.id;
});
// 2. Autorización (Scope)
app.get('/api/drive/files', async (req, res) => {
// Verificar permiso de Google Drive
const hasPermission = await checkGoogleScope(
req.user.accessToken,
'https://www.googleapis.com/auth/drive.readonly'
);
if (!hasPermission) {
return res.status(403).json({
error: 'Permiso requerido',
message: 'Por favor otorga acceso a Google Drive'
});
}
// Tiene permisos → Llamar API
const files = await listDriveFiles(req.user.accessToken);
res.json(files);
});
// ========== Resumen comparativo ==========
Autenticación (Authentication)
- Pregunta: "¿Quién eres?"
- Propósito: Verificar identidad
- Ejemplos:
* Ingresar contraseña
* Reconocimiento de huella
* Iniciar sesión con Google
- Código HTTP: 401 Unauthorized
Autorización (Authorization)
- Pregunta: "¿Qué puedes hacer?"
- Propósito: Verificar permisos de acceso
- Ejemplos:
* Permisos de administrador
* Permiso de lectura de Google Drive
* OAuth Scope
- Código HTTP: 403 Forbidden
// ¡Ambos son necesarios!
// 1. Primero autenticación (inicio de sesión)
// 2. Luego verificar permisos (permitir acceso)
🎓 Próximos pasos
Si has comprendido OAuth 2.0, prueba aprender lo siguiente:
- Token JWT (documento en preparación) - Autenticación basada en tokens
- ¿Qué es HTTPS? (documento en preparación) - Comunicación segura
- ¿Qué es CORS? (documento en preparación) - Seguridad de API
Práctica
# ========== 1. Práctica de Google OAuth ==========
# Crear proyecto
mkdir oauth-demo
cd oauth-demo
npm init -y
# Instalar paquetes
npm install express axios express-session dotenv
# Crear archivo .env
cat > .env << EOF
GOOGLE_CLIENT_ID=tu_client_id
GOOGLE_CLIENT_SECRET=tu_client_secret
SESSION_SECRET=tu_session_secret
EOF
# Escribir server.js y ejecutar
node server.js
# Acceder a http://localhost:3000
# ========== 2. Práctica de Passport.js ==========
npm install passport passport-google-oauth20
# ========== 3. Implementar múltiples inicios de sesión sociales ==========
npm install passport-github2 passport-facebook
# Soportar Google, GitHub, Facebook
🎬 Conclusión
OAuth 2.0 es el protocolo de autenticación estándar de la web moderna:
- Seguridad: Autenticación segura sin compartir contraseñas
- Conveniencia: Registro rápido con inicio de sesión social
- Control de permisos: Otorgar solo los permisos necesarios
- Estándar: Soportado por todos los servicios principales
¡Construye un sistema de autenticación seguro y conveniente! 🔐