Autenticación de APIs
Implementa autenticación segura de APIs con claves API, tokens JWT y OAuth 2.0
¿Por qué Autenticación de APIs?
La Autenticación de APIs verifica la identidad de los clientes que realizan solicitudes a tu API. Asegura que solo usuarios y aplicaciones autorizadas puedan acceder a recursos protegidos, previniendo acceso no autorizado y violaciones de datos.
Diferentes métodos de autenticación se adaptan a diferentes casos de uso—desde claves API simples para comunicación servidor a servidor hasta OAuth 2.0 para autorización de aplicaciones de terceros.
🔐 Autenticación vs Autorización
Autenticación (AuthN)
"¿Quién eres?"
Verificar identidad con credenciales como contraseñas, tokens o certificados.
Autorización (AuthZ)
"¿Qué puedes hacer?"
Determinar qué recursos/acciones puede acceder un usuario verificado.
1. Claves API
La forma más simple de autenticación—una clave secreta enviada con cada solicitud:
// Autenticación con Clave API
// Usualmente enviada en headers o parámetros de consulta
// Método 1: Header (preferido)
const response = await fetch('https://api.example.com/data', {
headers: {
'X-API-Key': 'tu-clave-api-aqui'
}
});
// Método 2: Parámetro de consulta (menos seguro - visible en logs)
const response = await fetch(
'https://api.example.com/data?api_key=tu-clave-api-aqui'
);
// Método 3: Basic Auth con clave API como usuario
const credentials = btoa('tu-clave-api:'); // Codificar en Base64
const response = await fetch('https://api.example.com/data', {
headers: {
'Authorization': `Basic ${credentials}`
}
});
// Cliente API reutilizable con clave API
class ApiKeyClient {
constructor(baseUrl, apiKey) {
this.baseUrl = baseUrl;
this.apiKey = apiKey;
}
async request(endpoint, options = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
'X-API-Key': this.apiKey,
'Content-Type': 'application/json',
...options.headers
}
});
return response.json();
}
}
// Uso
const api = new ApiKeyClient('https://api.weather.com', 'abc123');
const weather = await api.request('/current?city=London');
⚠️ Seguridad de Claves API
- • Nunca expongas claves API en código del lado del cliente
- • Usa variables de entorno para almacenamiento
- • Rota las claves regularmente
- • Usa claves diferentes para diferentes entornos
2. JWT (JSON Web Tokens)
Tokens autocontenidos que codifican información del usuario y están firmados criptográficamente:
// Estructura JWT: header.payload.signature
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
// eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4ifQ.
// SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Flujo de Autenticación JWT
class AuthClient {
constructor(baseUrl) {
this.baseUrl = baseUrl;
this.accessToken = null;
this.refreshToken = null;
}
// Paso 1: Iniciar sesión para obtener tokens
async login(email, password) {
const response = await fetch(`${this.baseUrl}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
if (!response.ok) {
throw new Error('Error al iniciar sesión');
}
const data = await response.json();
this.accessToken = data.accessToken;
this.refreshToken = data.refreshToken;
// Almacenar token de refresco de forma segura
localStorage.setItem('refreshToken', this.refreshToken);
return data.user;
}
// Paso 2: Usar token de acceso para solicitudes API
async request(endpoint, options = {}) {
const response = await fetch(`${this.baseUrl}${endpoint}`, {
...options,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
});
// Si el token expiró, refrescar y reintentar
if (response.status === 401) {
await this.refreshAccessToken();
return this.request(endpoint, options);
}
return response.json();
}
// Paso 3: Refrescar token de acceso expirado
async refreshAccessToken() {
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
if (!response.ok) {
// Token de refresco también expiró - redirigir a login
this.logout();
throw new Error('Sesión expirada');
}
const data = await response.json();
this.accessToken = data.accessToken;
}
// Paso 4: Cerrar sesión
logout() {
this.accessToken = null;
this.refreshToken = null;
localStorage.removeItem('refreshToken');
window.location.href = '/login';
}
}
// Uso
const auth = new AuthClient('https://api.example.com');
await auth.login('usuario@example.com', 'password123');
const profile = await auth.request('/user/profile');
const posts = await auth.request('/user/posts');
Decodificando Tokens JWT
// Los tokens JWT pueden decodificarse (pero no verificarse) del lado del cliente
// ¡Nunca confíes en datos decodificados para decisiones de seguridad!
function decodeJWT(token) {
try {
const [header, payload, signature] = token.split('.');
// Decodificar el payload en Base64
const decodedPayload = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
return JSON.parse(decodedPayload);
} catch {
return null;
}
}
// Verificar si el token expiró
function isTokenExpired(token) {
const payload = decodeJWT(token);
if (!payload || !payload.exp) return true;
// exp está en segundos, Date.now() está en milisegundos
return Date.now() >= payload.exp * 1000;
}
// Obtener tiempo hasta la expiración
function getTokenExpiresIn(token) {
const payload = decodeJWT(token);
if (!payload || !payload.exp) return 0;
return Math.max(0, payload.exp * 1000 - Date.now());
}
// Uso
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';
const payload = decodeJWT(token);
console.log('ID de Usuario:', payload.sub);
console.log('Expira:', new Date(payload.exp * 1000));
console.log('¿Está expirado?:', isTokenExpired(token));
console.log('Expira en:', getTokenExpiresIn(token) / 1000, 'segundos');
// Ejemplo de payload JWT
{
"sub": "user-123", // Sujeto (ID de usuario)
"name": "Juan Pérez", // Claim personalizado
"email": "juan@test.com", // Claim personalizado
"role": "admin", // Claim personalizado
"iat": 1704067200, // Emitido en
"exp": 1704070800 // Expira en
}
3. OAuth 2.0
Protocolo estándar de la industria para autorización, permitiendo que aplicaciones de terceros accedan a datos del usuario:
Tipos de Flujo OAuth 2.0
- Código de Autorización - Para aplicaciones del lado del servidor (más seguro)
- PKCE - Para SPAs y aplicaciones móviles
- Credenciales de Cliente - Para máquina a máquina
- Implícito - Obsoleto, no usar
// OAuth 2.0 con PKCE (Proof Key for Code Exchange)
// Mejor para aplicaciones basadas en navegador
class OAuthClient {
constructor(config) {
this.clientId = config.clientId;
this.redirectUri = config.redirectUri;
this.authorizationUrl = config.authorizationUrl;
this.tokenUrl = config.tokenUrl;
this.scope = config.scope;
}
// Paso 1: Generar desafío PKCE
async generatePKCE() {
// Generar verificador aleatorio
const array = new Uint8Array(32);
crypto.getRandomValues(array);
const verifier = btoa(String.fromCharCode(...array))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
// Crear desafío SHA-256
const encoder = new TextEncoder();
const data = encoder.encode(verifier);
const hash = await crypto.subtle.digest('SHA-256', data);
const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
.replace(/\+/g, '-')
.replace(/\//g, '_')
.replace(/=/g, '');
return { verifier, challenge };
}
// Paso 2: Redirigir a autorización
async startAuth() {
const { verifier, challenge } = await this.generatePKCE();
const state = crypto.randomUUID();
// Almacenar para verificación posterior
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', state);
// Construir URL de autorización
const params = new URLSearchParams({
response_type: 'code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
scope: this.scope,
state: state,
code_challenge: challenge,
code_challenge_method: 'S256'
});
// Redirigir al servidor de autenticación
window.location.href = `${this.authorizationUrl}?${params}`;
}
// Paso 3: Manejar callback e intercambiar código por tokens
async handleCallback() {
const params = new URLSearchParams(window.location.search);
const code = params.get('code');
const state = params.get('state');
const error = params.get('error');
// Verificar errores
if (error) {
throw new Error(params.get('error_description') || error);
}
// Verificar state
const savedState = sessionStorage.getItem('oauth_state');
if (state !== savedState) {
throw new Error('Parámetro state inválido');
}
// Obtener verificador
const verifier = sessionStorage.getItem('pkce_verifier');
// Intercambiar código por tokens
const response = await fetch(this.tokenUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'authorization_code',
client_id: this.clientId,
redirect_uri: this.redirectUri,
code: code,
code_verifier: verifier
})
});
if (!response.ok) {
throw new Error('Intercambio de token falló');
}
const tokens = await response.json();
// Limpiar
sessionStorage.removeItem('pkce_verifier');
sessionStorage.removeItem('oauth_state');
return tokens;
}
}
// Uso - ejemplo de Google OAuth
const oauth = new OAuthClient({
clientId: 'tu-client-id.apps.googleusercontent.com',
redirectUri: 'https://tuapp.com/callback',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scope: 'openid email profile'
});
// Iniciar login
document.getElementById('login').onclick = () => oauth.startAuth();
// En la página de callback
if (window.location.pathname === '/callback') {
const tokens = await oauth.handleCallback();
console.log('Token de acceso:', tokens.access_token);
}
Ejemplos de Login Social
// Usando OAuth con proveedores populares
// OAuth de GitHub
const githubAuth = new OAuthClient({
clientId: 'tu-github-client-id',
redirectUri: 'https://tuapp.com/auth/github/callback',
authorizationUrl: 'https://github.com/login/oauth/authorize',
tokenUrl: 'https://github.com/login/oauth/access_token',
scope: 'user:email read:user'
});
// Después de obtener el token de acceso, obtener info del usuario
async function getGitHubUser(accessToken) {
const response = await fetch('https://api.github.com/user', {
headers: {
'Authorization': `Bearer ${accessToken}`,
'Accept': 'application/json'
}
});
return response.json();
}
// OAuth de Google
const googleAuth = new OAuthClient({
clientId: 'tu-google-client-id',
redirectUri: 'https://tuapp.com/auth/google/callback',
authorizationUrl: 'https://accounts.google.com/o/oauth2/v2/auth',
tokenUrl: 'https://oauth2.googleapis.com/token',
scope: 'openid email profile'
});
// OAuth de Microsoft
const microsoftAuth = new OAuthClient({
clientId: 'tu-azure-client-id',
redirectUri: 'https://tuapp.com/auth/microsoft/callback',
authorizationUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
scope: 'openid email profile User.Read'
});
Almacenamiento Seguro de Tokens
// Mejores prácticas de almacenamiento de tokens
// ❌ NO almacenar en localStorage (vulnerable a XSS)
localStorage.setItem('accessToken', token);
// ✅ Para tokens de acceso: Mantener solo en memoria
class TokenManager {
#accessToken = null; // Campo privado
setAccessToken(token) {
this.#accessToken = token;
}
getAccessToken() {
return this.#accessToken;
}
clearTokens() {
this.#accessToken = null;
}
}
// ✅ Para tokens de refresco: Usar cookies httpOnly (establecidas por el servidor)
// Respuesta del servidor establece cookie:
// Set-Cookie: refreshToken=xxx; HttpOnly; Secure; SameSite=Strict
// ✅ Si debes usar almacenamiento, encripta datos sensibles
class SecureStorage {
constructor(encryptionKey) {
this.key = encryptionKey;
}
async encrypt(data) {
const encoder = new TextEncoder();
const dataBuffer = encoder.encode(JSON.stringify(data));
const iv = crypto.getRandomValues(new Uint8Array(12));
const encrypted = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
this.key,
dataBuffer
);
return btoa(JSON.stringify({
iv: Array.from(iv),
data: Array.from(new Uint8Array(encrypted))
}));
}
async decrypt(encryptedString) {
const { iv, data } = JSON.parse(atob(encryptedString));
const decrypted = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv: new Uint8Array(iv) },
this.key,
new Uint8Array(data)
);
const decoder = new TextDecoder();
return JSON.parse(decoder.decode(decrypted));
}
}
// Tiempo de espera de sesión - logout automático
function setupSessionTimeout(timeoutMinutes = 30) {
let timeoutId;
function resetTimer() {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
auth.logout();
alert('Sesión expirada. Por favor inicia sesión de nuevo.');
}, timeoutMinutes * 60 * 1000);
}
// Reiniciar con actividad del usuario
['click', 'keydown', 'mousemove', 'scroll'].forEach(event => {
document.addEventListener(event, resetTimer);
});
resetTimer();
}
💡 Mejores Prácticas de Autenticación
- ✓ Usa HTTPS siempre - Nunca envíes tokens sobre HTTP
- ✓ Tokens de acceso de corta duración - 15-60 minutos máximo
- ✓ Refrescar tokens de forma segura - Cookies HttpOnly preferidas
- ✓ Implementar rotación de tokens - Nuevo token de refresco en cada uso
- ✓ Usar PKCE para SPAs - Protege contra intercepción de código
- ✓ Validar parámetro state - Previene ataques CSRF