Introducción
Proteger una API implica mucho más que agregar un campo de contraseña a un formulario. Necesitas un esquema en el que el servidor pueda confiar en que una solicitud realmente proviene de quien dice ser. Los JSON Web Tokens resuelven esto de forma elegante: el servidor firma un token en el momento del login, y las solicitudes posteriores llevan ese token como prueba de identidad. Sin tabla de sesiones, sin estado compartido.
¿Qué es JWT?
JSON Web Token (JWT) es un estándar abierto (RFC 7519) para transmitir información entre partes como un objeto JSON firmado. Dado que el token está firmado —ya sea con una clave secreta o con un par de claves pública/privada— el servidor puede verificar que no ha sido manipulado.
¿Por qué usar JWT?
Los JWT son lo suficientemente compactos como para caber en un encabezado HTTP. Son autocontenidos: el token lleva la identidad del usuario y cualquier claim necesario, por lo que el servidor no tiene que consultar la base de datos en cada solicitud. Y dado que no hay sesión en el servidor que mantener, la autenticación basada en JWT escala horizontalmente sin coordinación entre servidores.
Cómo funciona la Autenticación JWT
- El usuario envía las credenciales (usuario y contraseña).
- El servidor las valida y firma un JWT con una clave secreta.
- El servidor devuelve el token en el cuerpo de la respuesta.
- El cliente almacena el token (localStorage o una cookie HTTP-only).
- Cada solicitud posterior incluye el token en el encabezado
Authorization. - El servidor verifica la firma antes de procesar la solicitud.
Implementando Autenticación JWT en TypeScript
A continuación, una implementación paso a paso en Node.js con TypeScript.
Requisitos Previos
Node.js instalado y familiaridad básica con TypeScript y Express.js.
Configurando el Proyecto
-
Inicializa el proyecto
mkdir jwt-auth-typescript cd jwt-auth-typescript npm init -y -
Instala las dependencias
npm install express jsonwebtoken bcryptjs cors dotenv -
Instala TypeScript y los tipos
npm install --save-dev typescript @types/express @types/jsonwebtoken @types/bcryptjs @types/cors -
Inicializa la configuración de TypeScript
npx tsc --init
Estructura del Proyecto
jwt-auth-typescript/
├── src/
│ ├── index.ts
│ ├── routes/
│ │ └── auth.ts
│ ├── controllers/
│ │ └── authController.ts
│ ├── models/
│ └── user.ts
├── package.json
├── tsconfig.json
└── .env
Configurando TypeScript (tsconfig.json)
Asegúrate de que el tsconfig.json incluya:
{
"compilerOptions": {
"target": "ES6",
"module": "commonjs",
"outDir": "./dist",
"rootDir": "./src",
"strict": true
}
}
Configurando las Variables de Entorno
Crea un archivo .env en el directorio raíz:
PORT=5000
JWT_SECRET=your_jwt_secret_key
Creando el Servidor (index.ts)
import express from 'express';
import cors from 'cors';
import dotenv from 'dotenv';
import authRoutes from './routes/auth';
dotenv.config();
const app = express();
const PORT = process.env.PORT || 5000;
app.use(cors());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Creando el Modelo de Usuario (models/user.ts)
En una aplicación real usarías una base de datos. Para este ejemplo, un array en memoria es suficiente para demostrar el flujo de autenticación.
interface User {
id: number;
username: string;
password: string;
}
const users: User[] = [];
export default users;
export type { User };
Creando las Rutas de Autenticación (routes/auth.ts)
import { Router } from 'express';
import { register, login, protectedRoute } from '../controllers/authController';
const router = Router();
router.post('/register', register);
router.post('/login', login);
router.get('/protected', protectedRoute);
export default router;
Implementando el Controller (controllers/authController.ts)
import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcryptjs';
import users, { User } from '../models/user';
const JWT_SECRET = process.env.JWT_SECRET || 'secret_key';
export const register = async (req: Request, res: Response) => {
const { username, password } = req.body;
// Check if user exists
const userExists = users.find((user) => user.username === username);
if (userExists) {
return res.status(400).json({ message: 'User already exists' });
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 10);
// Create user
const newUser: User = {
id: users.length + 1,
username,
password: hashedPassword,
};
users.push(newUser);
res.status(201).json({ message: 'User registered successfully' });
};
export const login = async (req: Request, res: Response) => {
const { username, password } = req.body;
// Find user
const user = users.find((user) => user.username === username);
if (!user) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Check password
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(400).json({ message: 'Invalid credentials' });
}
// Sign token
const token = jwt.sign({ id: user.id, username: user.username }, JWT_SECRET, { expiresIn: '1h' });
res.json({ token });
};
export const protectedRoute = (req: Request, res: Response, next: NextFunction) => {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
(req as any).user = user;
next();
});
};
Protegiendo Rutas
Mueve la lógica de verificación a un array de middleware para poder reutilizarla en distintas rutas:
export const protectedRoute = [
(req: Request, res: Response, next: NextFunction) => {
// Middleware to verify JWT
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) {
return res.sendStatus(401);
}
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.sendStatus(403);
(req as any).user = user;
next();
});
},
(req: Request, res: Response) => {
// Protected route logic
res.json({ message: 'This is a protected route', user: (req as any).user });
}
];
Probando la Aplicación
-
Compila TypeScript
npx tsc -
Inicia el servidor
node dist/index.js -
Registra un usuario
POST http://localhost:5000/api/auth/register Content-Type: application/json { "username": "testuser", "password": "password123" } -
Inicia sesión
POST http://localhost:5000/api/auth/login Content-Type: application/json { "username": "testuser", "password": "password123" }Recibirás un token JWT en la respuesta.
-
Accede a la ruta protegida
GET http://localhost:5000/api/auth/protected Authorization: Bearer <your_jwt_token>Deberías recibir una respuesta indicando que el acceso fue concedido.
Consideraciones de Seguridad
- Usa siempre HTTPS en producción. Un JWT enviado por HTTP simple puede ser interceptado.
- Define tiempos de expiración cortos. Una hora es razonable para tokens de acceso; usa refresh tokens para sesiones más largas.
- Prefiere almacenar los tokens en cookies HTTP-only, no en localStorage. El localStorage es legible por cualquier JavaScript de la página.
- Valida todas las entradas del usuario para prevenir ataques de inyección.
Conclusión
La autenticación JWT es una solución práctica para APIs sin estado. La implementación anterior cubre el registro, el hash de contraseñas con bcrypt, la firma y verificación de tokens, y la protección de rutas como middleware. A partir de esta base, los próximos pasos naturales son la rotación de refresh tokens y migrar el almacenamiento en memoria a una base de datos real.
Lectura Adicional
Comparte tu Opinión
Si este contenido te resultó útil o tienes preguntas, contáctame en Twitter.
Por Samuel Fajreldines, especialista en JavaScript, TypeScript, DevOps y Arquitectura Serverless.