Saltar al contenido principal

🔐 ¿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:

  1. Token JWT (documento en preparación) - Autenticación basada en tokens
  2. ¿Qué es HTTPS? (documento en preparación) - Comunicación segura
  3. ¿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! 🔐