¿Trabajas con APIs y autenticación basada en tokens? Los JSON Web Tokens (JWT) se han convertido en el estándar de facto para la autenticación y autorización en aplicaciones modernas. En esta guía completa, aprenderás todo sobre JWT y cómo usar nuestra herramienta gratuita de decodificación y codificación.
Un JSON Web Token (JWT) es un estándar abierto (RFC 7519) que define una forma compacta y autocontenida de transmitir información entre partes como un objeto JSON. Esta información puede ser verificada y confiada porque está firmada digitalmente.
El caso de uso más común. Una vez que el usuario inicia sesión, cada solicitud subsecuente incluye el JWT, permitiendo al usuario acceder a rutas, servicios y recursos permitidos con ese token.
javascript
// Ejemplo de autenticación con JWT
POST /api/login
Content-Type: application/json
{
"username": "usuario",
"password": "contraseña"
}
// Respuesta
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
// Uso en solicitudes posteriores
GET /api/datos-protegidos
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
JWT es una buena forma de transmitir información de forma segura entre partes, ya que pueden ser firmados para verificar la identidad del remitente y asegurar que el contenido no ha sido manipulado.
Permite a los usuarios autenticarse una vez y acceder a múltiples aplicaciones sin volver a iniciar sesión.
Un JWT consta de tres partes separadas por puntos (.):
header.payload.signature
El header típicamente consiste en dos partes:
json
{
"alg": "HS256",
"typ": "JWT"
}
| Campo | Descripción | Valores comunes |
|---|---|---|
| alg | Algoritmo de firma | HS256, HS512, RS256, RS512, ES256 |
| typ | Tipo de token | JWT |
| kid | Key ID | Identificador de la clave usada |
El payload contiene las claims (afirmaciones). Hay tres tipos de claims:
Son claims predefinidos pero opcionales:
json
{
"iss": "https://mi-app.com",
"sub": "1234567890",
"aud": "https://api.mi-app.com",
"exp": 1735689600,
"nbf": 1735603200,
"iat": 1735603200,
"jti": "unique-token-id-123"
}
Descripción de claims estándar:
| Claim | Nombre | Descripción | Tipo |
|---|---|---|---|
| iss | Issuer | Emisor del token | String |
| sub | Subject | Sujeto (normalmente user ID) | String |
| aud | Audience | Audiencia destinataria | String o Array |
| exp | Expiration Time | Tiempo de expiración | NumericDate |
| nbf | Not Before | No válido antes de | NumericDate |
| iat | Issued At | Tiempo de emisión | NumericDate |
| jti | JWT ID | Identificador único del token | String |
Pueden ser definidos libremente pero deberían estar registrados en IANA JWT Registry o usar nombres con colisión resistente.
json
{
"name": "John Doe",
"email": "john@example.com",
"https://mi-app.com/roles": ["admin", "user"]
}
Claims personalizados acordados entre las partes que usan el JWT:
json
{
"userId": "12345",
"role": "admin",
"permissions": ["read", "write", "delete"],
"department": "IT",
"isEmailVerified": true
}
La firma se crea tomando:
javascript
// Pseudocódigo de cómo se genera la firma
HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload),
secret
)
Ejemplo completo de JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Desglosado:
Header: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
Payload: eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
Signature: SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
- Usuario → Login (username/password) → Servidor
- Servidor valida credenciales
- Servidor genera JWT firmado
- Servidor → JWT → Usuario
- Usuario almacena JWT (localStorage, cookie, etc.)
- Usuario → Request + JWT en Authorization header → Servidor
- Servidor valida firma del JWT
- Si válido → Procesa solicitud y responde
Si inválido → Retorna 401 Unauthorized
javascript
// El servidor NO decodifica el JWT ciegamente
// Siempre valida la firma primero
- Recibir JWT del header Authorization
- Extraer header y algoritmo
- Verificar que el algoritmo es el esperado
- Validar firma usando la clave secreta/pública
- Si firma es válida:
- Decodificar payload
- Verificar exp (no expirado)
- Verificar nbf (ya es válido)
- Verificar iss, aud (si aplica)
- Procesar solicitud
- Si firma inválida o expirado:
- Rechazar con 401 Unauthorized
Los JWT usan Base64 URL encoding (no Base64 estándar):
javascript
// Base64 URL decode
function base64UrlDecode(str) {
// Reemplazar caracteres URL-safe
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
// Agregar padding si es necesario
const pad = str.length % 4;
if (pad) {
base64 += '='.repeat(4 - pad);
}
return atob(base64);
}
// Decodificar JWT
function decodeJWT(token) {
const [headerB64, payloadB64, signature] = token.split('.');
const header = JSON.parse(base64UrlDecode(headerB64));
const payload = JSON.parse(base64UrlDecode(payloadB64));
return { header, payload, signature };
}
Nuestra herramienta de decodificación JWT simplifica el proceso:
javascript
// 1. Crear header
const header = {
alg: 'HS256',
typ: 'JWT'
};
// 2. Crear payload
const payload = {
sub: '1234567890',
name: 'John Doe',
iat: Math.floor(Date.now() / 1000),
exp: Math.floor(Date.now() / 1000) + (60 * 60) // 1 hora
};
// 3. Codificar header y payload
const headerB64 = base64UrlEncode(JSON.stringify(header));
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
// 4. Crear firma
const signature = HMACSHA256(
headerB64 + '.' + payloadB64,
'tu-secreto-super-seguro'
);
// 5. Combinar todo
const jwt = headerB64 + '.' + payloadB64 + '.' + signature;
javascript
async function createJWT(payload, secret) {
const header = { alg: 'HS256', typ: 'JWT' };
// Codificar header y payload
const headerB64 = base64UrlEncode(JSON.stringify(header));
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
const message = headerB64 + '.' + payloadB64;
// Importar clave
const encoder = new TextEncoder();
const keyData = encoder.encode(secret);
const key = await crypto.subtle.importKey(
'raw',
keyData,
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
// Firmar
const signature = await crypto.subtle.sign(
'HMAC',
key,
encoder.encode(message)
);
// Convertir a base64url
const signatureB64 = base64UrlEncode(
String.fromCharCode(...new Uint8Array(signature))
);
return message + '.' + signatureB64;
}
json
// Header malicioso
{
"alg": "none",
"typ": "JWT"
}
Prevención:
javascript
// Siempre validar el algoritmo
if (header.alg === 'none') {
throw new Error('Algoritmo none no permitido');
}
Atacante cambia RS256 a HS256 y firma con la clave pública (que el servidor conoce).
Prevención:javascript
// Especificar explícitamente el algoritmo esperado
const expectedAlgorithm = 'RS256';
if (header.alg !== expectedAlgorithm) {
throw new Error('Algoritmo no permitido');
}
El payload es solo codificado (Base64), NO encriptado. Cualquiera puede decodificarlo.
Prevención:javascript
// ❌ NUNCA hacer esto
const payload = {
userId: 123,
password: 'contraseña123', // ¡NUNCA!
ssn: '123-45-6789', // ¡NUNCA!
creditCard: '1234-5678-...' // ¡NUNCA!
};
// ✅ Solo información no sensible
const payload = {
userId: 123,
email: 'user@example.com',
role: 'admin',
iat: Date.now()
};
javascript
function validateToken(token) {
const { payload } = decodeAndVerify(token);
// Verificar expiración
if (payload.exp && payload.exp < Date.now() / 1000) {
throw new Error('Token expirado');
}
// Verificar not-before
if (payload.nbf && payload.nbf > Date.now() / 1000) {
throw new Error('Token aún no válido');
}
return payload;
}
javascript
// ❌ Secretos débiles
const secret = 'secret';
const secret = '123456';
const secret = 'password';
// ✅ Secreto fuerte
const secret = process.env.JWT_SECRET; // Variable de entorno
// Ejemplo: 'aB3$kL9#mN2@pQ7*rS5!tU8&vW1^xY4%zC6+dE0-fG'
| Método | Seguridad | Accesibilidad | Vulnerabilidad |
|---|---|---|---|
| localStorage | ❌ Baja | JavaScript | XSS |
| sessionStorage | ⚠️ Media | JavaScript | XSS |
| Cookie (HttpOnly) | ✅ Alta | Solo servidor | CSRF (mitigable) |
| Memory (variable) | ✅ Alta | Solo sesión | Se pierde al recargar |
javascript
// Usar cookie HttpOnly con SameSite
res.cookie('token', jwt, {
httpOnly: true, // No accesible desde JavaScript
secure: true, // Solo HTTPS
sameSite: 'strict', // Protección CSRF
maxAge: 3600000 // 1 hora
});
❌ http://api.ejemplo.com/login
✅ https://api.ejemplo.com/login
javascript
const payload = {
userId: 123,
iat: Math.floor(Date.now() / 1000),
// Access token: 15 minutos
exp: Math.floor(Date.now() / 1000) + (15 * 60)
};
// Usar refresh tokens para renovar
javascript
// Access token (corta duración)
const accessToken = createJWT({
userId: 123,
type: 'access',
exp: now + (15 * 60) // 15 minutos
});
// Refresh token (larga duración)
const refreshToken = createJWT({
userId: 123,
type: 'refresh',
exp: now + (7 24 60 * 60) // 7 días
});
javascript
// Almacenar tokens revocados en Redis
const revokedTokens = new Set();
function revokeToken(token) {
revokedTokens.add(token);
// En producción: redis.sadd('revoked_tokens', token)
}
function isRevoked(token) {
return revokedTokens.has(token);
// En producción: redis.sismember('revoked_tokens', token)
}
javascript
function validateClaims(payload) {
const currentTime = Math.floor(Date.now() / 1000);
// Validar expiración
if (!payload.exp || payload.exp < currentTime) {
throw new Error('Token expirado');
}
// Validar emisor
if (payload.iss !== 'https://mi-app.com') {
throw new Error('Emisor inválido');
}
// Validar audiencia
if (payload.aud !== 'https://api.mi-app.com') {
throw new Error('Audiencia inválida');
}
return true;
}
| Característica | JWT | Sesiones |
|---|---|---|
| Almacenamiento | Cliente (token) | Servidor (session store) |
| Escalabilidad | ✅ Excelente (stateless) | ⚠️ Requiere shared storage |
| Tamaño | ⚠️ Mayor (>200 bytes) | ✅ Menor (~32 bytes ID) |
| Revocación | ⚠️ Compleja (requiere blacklist) | ✅ Fácil (borrar del store) |
| Cross-domain | ✅ Fácil | ⚠️ Requiere CORS/config |
| Mobile-friendly | ✅ Excelente | ⚠️ Limitado |
JWT y OAuth 2.0 no son competidores, son complementarios:
OAuth 2.0 puede usar JWT como formato de access token.
| Característica | JWT | API Key |
|---|---|---|
| Información | ✅ Contiene claims | ❌ Solo identificador |
| Expiración | ✅ Integrada | ⚠️ Manual |
| Rotación | ✅ Fácil (refresh token) | ⚠️ Manual |
| User context | ✅ Incluido en payload | ❌ Requiere lookup |
| Simplicidad | ⚠️ Media | ✅ Alta |
javascript
const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();
// Generar JWT
app.post('/login', (req, res) => {
const { username, password } = req.body;
// Validar credenciales
if (validateUser(username, password)) {
const token = jwt.sign(
{ userId: user.id, email: user.email },
process.env.JWT_SECRET,
{ expiresIn: '1h' }
);
res.json({ token });
} else {
res.status(401).json({ error: 'Credenciales inválidas' });
}
});
// Middleware de autenticación
function authenticateToken(req, res, next) {
const token = req.headers['authorization']?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Token requerido' });
}
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
if (err) {
return res.status(403).json({ error: 'Token inválido' });
}
req.user = user;
next();
});
}
// Ruta protegida
app.get('/protected', authenticateToken, (req, res) => {
res.json({ message: 'Acceso permitido', user: req.user });
});
python
from flask import Flask, request, jsonify
import jwt
import datetime
app = Flask(name)
app.config['SECRET_KEY'] = 'tu-secreto-super-seguro'
@app.route('/login', methods=['POST'])
def login():
data = request.get_json()
if validate_user(data['username'], data['password']):
token = jwt.encode({
'user_id': user.id,
'email': user.email,
'exp': datetime.datetime.utcnow() + datetime.timedelta(hours=1)
}, app.config['SECRET_KEY'], algorithm='HS256')
return jsonify({'token': token})
return jsonify({'error': 'Credenciales inválidas'}), 401
Decorador de autenticación
def token_required(f):
def decorator(args, *kwargs):
token = request.headers.get('Authorization', '').replace('Bearer ', '')
if not token:
return jsonify({'error': 'Token requerido'}), 401
try:
data = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
currentuser = data['userid']
except:
return jsonify({'error': 'Token inválido'}), 403
return f(current_user, args, *kwargs)
return decorator
@app.route('/protected')
@token_required
def protected(current_user):
return jsonify({'message': 'Acceso permitido', 'user': current_user})
php
use Firebase\JWT\JWT;
use Firebase\JWT\Key;
// Generar token
public function login(Request $request) {
$credentials = $request->only('email', 'password');
if (Auth::attempt($credentials)) {
$user = Auth::user();
$payload = [
'iss' => 'https://mi-app.com',
'sub' => $user->id,
'email' => $user->email,
'iat' => time(),
'exp' => time() + 3600
];
$token = JWT::encode($payload, env('JWT_SECRET'), 'HS256');
return response()->json(['token' => $token]);
}
return response()->json(['error' => 'Unauthorized'], 401);
}
// Middleware
public function handle($request, Closure $next) {
$token = $request->bearerToken();
if (!$token) {
return response()->json(['error' => 'Token required'], 401);
}
try {
$decoded = JWT::decode($token, new Key(env('JWT_SECRET'), 'HS256'));
$request->user = $decoded;
return $next($request);
} catch (Exception $e) {
return response()->json(['error' => 'Invalid token'], 403);
}
}
javascript
const payload = {
userId: 123,
tenantId: 'company-abc',
role: 'admin',
permissions: ['read:all', 'write:own', 'delete:own']
};
javascript
// Gateway valida el token una vez
// Los microservicios confían en el token validado
// Gateway
if (validateToken(token)) {
// Propagar a microservicios
forward(request, { headers: { 'X-User-Id': payload.userId } });
}
// Microservicio
// Solo lee el header, no re-valida
const userId = request.headers['x-user-id'];
javascript
const payload = {
userId: 123,
actingAs: 456, // Usuario 123 actúa como 456
scope: ['read:profile', 'write:posts'],
delegatedBy: 456
};
jsonwebtoken: Más popular, fácil de usarjose: Moderna, completa, soporta JWEPyJWT: Estándar de factopython-jose: Completa, soporta JWEfirebase/php-jwt: Muy usadalcobucci/jwt: Moderna, POOjjwt: Completa y robustaauth0/java-jwt: Mantenida por Auth0✨ Decodificación instantánea:
🔐 Codificación segura:
🛡️ 100% privado:
📋 Copiar con un click:
Los JWT son seguros cuando se usan correctamente. La seguridad depende de:
Sí, DECODIFICAR no requiere la clave. El JWT está solo codificado en Base64, no encriptado. Sin embargo, para VERIFICAR la firma sí necesitas la clave secreta.
No es recomendado. localStorage es vulnerable a ataques XSS. Mejor usar cookies HttpOnly o mantener el token en memoria.
Los JWT son stateless, pero puedes:
Sí, si usas una clave secreta fuerte y la mantienes segura. Para mayor seguridad en producción, considera RS256 (asimétrico).
El tamaño del JWT depende de la cantidad de información en el payload. Mantén el payload mínimo para reducir el tamaño.
Los JSON Web Tokens son una herramienta poderosa y versátil para autenticación y autorización en aplicaciones modernas. Cuando se implementan correctamente siguiendo las mejores prácticas de seguridad, ofrecen una solución escalable, eficiente y segura.
🔑 Estructura clara: Header, Payload, Signature
🛡️ Seguridad primero: Validar siempre la firma
⏱️ Expiración: Usar tiempos cortos con refresh tokens
🔒 Privacidad: Nunca incluir información sensible
📚 Estándar: Seguir RFC 7519
🌐 Universal: Soportado en todos los lenguajes
Nuestra herramienta gratuita te permite inspeccionar y crear JWTs de forma segura, rápida y privada. Todo procesado localmente en tu navegador.
Explorar más herramientas de desarrollo