Autenticación JWT
Este ejemplo muestra cómo implementar un sistema completo de autenticación JWT con registro, login, protección de rutas y refreshing de tokens.
Controlador de Autenticación
// src/controllers/auth.controller.ts
import { Request, Response } from '@foxframework/core';
import { AuthMiddleware } from '@foxframework/core/security';
import bcrypt from 'bcrypt';
import { EmailService } from '../services/email.service';
interface User {
id: number;
email: string;
password: string;
firstName: string;
lastName: string;
role: 'user' | 'admin';
isActive: boolean;
emailVerified: boolean;
createdAt: Date;
lastLogin?: Date;
}
interface RegisterRequest {
email: string;
password: string;
firstName: string;
lastName: string;
}
interface LoginRequest {
email: string;
password: string;
}
export class AuthController {
private users: User[] = [
{
id: 1,
email: 'admin@example.com',
password: '$2b$10$rYgQ8gF.XPKdGVLZfCKZHOGfQ8GfQ8GfQ8GfQ8GfQ8GfQ8GfQ8GfQ8', // "admin123"
firstName: 'Admin',
lastName: 'User',
role: 'admin',
isActive: true,
emailVerified: true,
createdAt: new Date('2024-01-01')
}
];
private refreshTokens: Map<string, { userId: number; expiresAt: Date }> = new Map();
private emailService = new EmailService();
// POST /auth/register
register = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password, firstName, lastName }: RegisterRequest = req.body;
// Validación de entrada
if (!email || !password || !firstName || !lastName) {
return res.status(400).json({
error: 'Validation failed',
message: 'All fields are required'
});
}
// Validar formato de email
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (!emailRegex.test(email)) {
return res.status(400).json({
error: 'Validation failed',
message: 'Invalid email format'
});
}
// Validar complejidad de contraseña
if (password.length < 8) {
return res.status(400).json({
error: 'Validation failed',
message: 'Password must be at least 8 characters long'
});
}
// Verificar si el usuario ya existe
const existingUser = this.users.find(u => u.email.toLowerCase() === email.toLowerCase());
if (existingUser) {
return res.status(409).json({
error: 'User already exists',
message: 'A user with this email already exists'
});
}
// Hash de la contraseña
const saltRounds = 12;
const hashedPassword = await bcrypt.hash(password, saltRounds);
// Crear nuevo usuario
const newUser: User = {
id: this.users.length + 1,
email: email.toLowerCase(),
password: hashedPassword,
firstName,
lastName,
role: 'user',
isActive: true,
emailVerified: false,
createdAt: new Date()
};
this.users.push(newUser);
// Generar token de verificación de email
const emailVerificationToken = AuthMiddleware.generateToken(
{ userId: newUser.id, type: 'email-verification' },
{
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '24h'
}
);
// Enviar email de verificación
await this.emailService.sendVerificationEmail(
newUser.email,
newUser.firstName,
emailVerificationToken
);
// No devolver la contraseña
const { password: _, ...userResponse } = newUser;
res.status(201).json({
message: 'User registered successfully. Please check your email to verify your account.',
data: userResponse
});
} catch (error) {
console.error('Registration error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred during registration'
});
}
};
// POST /auth/login
login = async (req: Request, res: Response): Promise<void> => {
try {
const { email, password }: LoginRequest = req.body;
// Validación de entrada
if (!email || !password) {
return res.status(400).json({
error: 'Validation failed',
message: 'Email and password are required'
});
}
// Buscar usuario
const user = this.users.find(u => u.email.toLowerCase() === email.toLowerCase());
if (!user) {
return res.status(401).json({
error: 'Authentication failed',
message: 'Invalid credentials'
});
}
// Verificar que el usuario esté activo
if (!user.isActive) {
return res.status(401).json({
error: 'Account disabled',
message: 'Your account has been disabled. Please contact support.'
});
}
// Verificar contraseña
const isPasswordValid = await bcrypt.compare(password, user.password);
if (!isPasswordValid) {
return res.status(401).json({
error: 'Authentication failed',
message: 'Invalid credentials'
});
}
// Generar tokens
const accessToken = AuthMiddleware.generateToken(
{
userId: user.id,
email: user.email,
role: user.role,
type: 'access'
},
{
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '15m'
}
);
const refreshToken = AuthMiddleware.generateToken(
{
userId: user.id,
type: 'refresh'
},
{
secret: process.env.JWT_REFRESH_SECRET || 'your-refresh-secret',
expiresIn: '7d'
}
);
// Guardar refresh token
this.refreshTokens.set(refreshToken, {
userId: user.id,
expiresAt: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 7 días
});
// Actualizar último login
user.lastLogin = new Date();
// Configurar cookie httpOnly para el refresh token
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7 días
});
// No devolver la contraseña
const { password: _, ...userResponse } = user;
res.json({
message: 'Login successful',
data: {
user: userResponse,
accessToken,
expiresIn: 900 // 15 minutes in seconds
}
});
} catch (error) {
console.error('Login error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred during login'
});
}
};
// POST /auth/refresh
refreshToken = async (req: Request, res: Response): Promise<void> => {
try {
const refreshToken = req.cookies.refreshToken;
if (!refreshToken) {
return res.status(401).json({
error: 'No refresh token',
message: 'Refresh token not found'
});
}
// Verificar si el token existe en nuestro store
const tokenData = this.refreshTokens.get(refreshToken);
if (!tokenData || tokenData.expiresAt < new Date()) {
this.refreshTokens.delete(refreshToken);
return res.status(401).json({
error: 'Invalid refresh token',
message: 'Refresh token is invalid or expired'
});
}
// Buscar usuario
const user = this.users.find(u => u.id === tokenData.userId);
if (!user || !user.isActive) {
this.refreshTokens.delete(refreshToken);
return res.status(401).json({
error: 'User not found',
message: 'User associated with token not found or inactive'
});
}
// Generar nuevo access token
const newAccessToken = AuthMiddleware.generateToken(
{
userId: user.id,
email: user.email,
role: user.role,
type: 'access'
},
{
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '15m'
}
);
res.json({
message: 'Token refreshed successfully',
data: {
accessToken: newAccessToken,
expiresIn: 900 // 15 minutes
}
});
} catch (error) {
console.error('Token refresh error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred while refreshing token'
});
}
};
// POST /auth/logout
logout = async (req: Request, res: Response): Promise<void> => {
try {
const refreshToken = req.cookies.refreshToken;
// Eliminar refresh token del store
if (refreshToken) {
this.refreshTokens.delete(refreshToken);
}
// Limpiar cookie
res.clearCookie('refreshToken');
res.json({
message: 'Logout successful'
});
} catch (error) {
console.error('Logout error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred during logout'
});
}
};
// GET /auth/profile
getProfile = async (req: Request, res: Response): Promise<void> => {
try {
const userId = req.user?.id;
if (!userId) {
return res.status(401).json({
error: 'Unauthorized',
message: 'User not authenticated'
});
}
const user = this.users.find(u => u.id === userId);
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'User profile not found'
});
}
// No devolver la contraseña
const { password: _, ...userResponse } = user;
res.json({
data: userResponse
});
} catch (error) {
console.error('Get profile error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred while fetching profile'
});
}
};
// POST /auth/verify-email
verifyEmail = async (req: Request, res: Response): Promise<void> => {
try {
const { token } = req.body;
if (!token) {
return res.status(400).json({
error: 'Validation failed',
message: 'Verification token is required'
});
}
// Verificar y decodificar el token JWT
let decoded: { userId: number; type: string };
try {
decoded = AuthMiddleware.verifyToken(token, {
secret: process.env.JWT_SECRET || 'your-secret-key'
}) as { userId: number; type: string };
} catch {
return res.status(400).json({
error: 'Invalid token',
message: 'Verification token is invalid or has expired'
});
}
if (decoded.type !== 'email-verification') {
return res.status(400).json({
error: 'Invalid token type',
message: 'Token is not an email verification token'
});
}
const user = this.users.find(u => u.id === decoded.userId);
if (!user) {
return res.status(404).json({
error: 'User not found',
message: 'No user associated with this verification token'
});
}
if (user.emailVerified) {
return res.status(409).json({
error: 'Already verified',
message: 'This email address has already been verified'
});
}
user.emailVerified = true;
// Send welcome email after successful verification
await this.emailService.sendWelcomeEmail(user.email, user.firstName);
res.json({
message: 'Email verified successfully'
});
} catch (error) {
console.error('Email verification error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred during email verification'
});
}
};
// POST /auth/forgot-password
forgotPassword = async (req: Request, res: Response): Promise<void> => {
try {
const { email } = req.body;
if (!email) {
return res.status(400).json({
error: 'Validation failed',
message: 'Email is required'
});
}
const user = this.users.find(u => u.email.toLowerCase() === email.toLowerCase());
// Por seguridad, siempre retornamos el mismo mensaje
// incluso si el usuario no existe
if (user) {
const resetToken = AuthMiddleware.generateToken(
{ userId: user.id, type: 'password-reset' },
{
secret: process.env.JWT_SECRET || 'your-secret-key',
expiresIn: '1h'
}
);
await this.emailService.sendPasswordResetEmail(
user.email,
user.firstName,
resetToken
);
}
res.json({
message: 'If an account with that email exists, we have sent a password reset link.'
});
} catch (error) {
console.error('Forgot password error:', error);
res.status(500).json({
error: 'Internal server error',
message: 'An error occurred while processing password reset'
});
}
};
}Servicio de Email
// src/services/email.service.ts
import nodemailer from 'nodemailer';
export class EmailService {
private transporter: nodemailer.Transporter;
constructor() {
this.transporter = nodemailer.createTransporter({
host: process.env.SMTP_HOST || 'smtp.gmail.com',
port: parseInt(process.env.SMTP_PORT || '587'),
secure: false, // true for 465, false for other ports
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
});
}
async sendVerificationEmail(email: string, firstName: string, token: string): Promise<void> {
const verificationUrl = `${process.env.FRONTEND_URL}/verify-email?token=${token}`;
const mailOptions = {
from: `"${process.env.APP_NAME}" <${process.env.SMTP_FROM}>`,
to: email,
subject: 'Verify your email address',
html: `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2>Welcome to ${process.env.APP_NAME}, ${firstName}!</h2>
<p>Thanks for signing up! Please verify your email address by clicking the button below:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${verificationUrl}"
style="background-color: #007bff; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Verify Email Address
</a>
</div>
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #666;">${verificationUrl}</p>
<p><small>This link will expire in 24 hours.</small></p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #666; font-size: 14px;">
If you didn't create an account, you can safely ignore this email.
</p>
</div>
`
};
await this.transporter.sendMail(mailOptions);
}
async sendPasswordResetEmail(email: string, firstName: string, token: string): Promise<void> {
const resetUrl = `${process.env.FRONTEND_URL}/reset-password?token=${token}`;
const mailOptions = {
from: `"${process.env.APP_NAME}" <${process.env.SMTP_FROM}>`,
to: email,
subject: 'Reset your password',
html: `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2>Password Reset Request</h2>
<p>Hi ${firstName},</p>
<p>You requested to reset your password. Click the button below to create a new password:</p>
<div style="text-align: center; margin: 30px 0;">
<a href="${resetUrl}"
style="background-color: #dc3545; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Reset Password
</a>
</div>
<p>If the button doesn't work, you can copy and paste this link into your browser:</p>
<p style="word-break: break-all; color: #666;">${resetUrl}</p>
<p><small>This link will expire in 1 hour.</small></p>
<hr style="border: none; border-top: 1px solid #eee; margin: 30px 0;">
<p style="color: #666; font-size: 14px;">
If you didn't request this reset, you can safely ignore this email.
</p>
</div>
`
};
await this.transporter.sendMail(mailOptions);
}
async sendWelcomeEmail(email: string, firstName: string): Promise<void> {
const mailOptions = {
from: `"${process.env.APP_NAME}" <${process.env.SMTP_FROM}>`,
to: email,
subject: `Welcome to ${process.env.APP_NAME}!`,
html: `
<div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
<h2>Welcome to ${process.env.APP_NAME}!</h2>
<p>Hi ${firstName},</p>
<p>Your email has been verified and your account is now active!</p>
<p>You can now:</p>
<ul>
<li>Access your dashboard</li>
<li>Update your profile</li>
<li>Start using all our features</li>
</ul>
<div style="text-align: center; margin: 30px 0;">
<a href="${process.env.FRONTEND_URL}/dashboard"
style="background-color: #28a745; color: white; padding: 12px 24px;
text-decoration: none; border-radius: 4px; display: inline-block;">
Go to Dashboard
</a>
</div>
<p>If you have any questions, feel free to contact our support team.</p>
<p>Best regards,<br>The ${process.env.APP_NAME} Team</p>
</div>
`
};
await this.transporter.sendMail(mailOptions);
}
}Configuración del Servidor
// src/server.ts
import { FoxFactory } from '@foxframework/core';
import { AuthMiddleware, SecurityMiddlewareCore } from '@foxframework/core/security';
import { AuthController } from './controllers/auth.controller';
import cookieParser from 'cookie-parser';
const authController = new AuthController();
const app = FoxFactory.createServer({
port: 3000,
middleware: [
// Cookie parser para refresh tokens
cookieParser(),
// Seguridad
SecurityMiddlewareCore.cors({
origin: process.env.FRONTEND_URL || 'http://localhost:3001',
credentials: true
}),
SecurityMiddlewareCore.securityHeaders(),
SecurityMiddlewareCore.rateLimit({
windowMs: 15 * 60 * 1000,
maxRequests: 100
})
],
routes: [
// Rutas públicas de autenticación
{
path: '/auth/register',
method: 'post',
handler: authController.register
},
{
path: '/auth/login',
method: 'post',
handler: authController.login,
middleware: [
SecurityMiddlewareCore.rateLimit({
windowMs: 15 * 60 * 1000,
maxRequests: 5, // Más restrictivo para login
message: 'Too many login attempts'
})
]
},
{
path: '/auth/refresh',
method: 'post',
handler: authController.refreshToken
},
{
path: '/auth/verify-email',
method: 'post',
handler: authController.verifyEmail
},
{
path: '/auth/forgot-password',
method: 'post',
handler: authController.forgotPassword
},
// Rutas protegidas
{
path: '/auth/profile',
method: 'get',
handler: authController.getProfile,
middleware: [
AuthMiddleware.jwt({
secret: process.env.JWT_SECRET || 'your-secret-key',
algorithms: ['HS256']
})
]
},
{
path: '/auth/logout',
method: 'post',
handler: authController.logout,
middleware: [
AuthMiddleware.optionalJwt({
secret: process.env.JWT_SECRET || 'your-secret-key',
algorithms: ['HS256']
})
]
}
]
});
app.start().then(() => {
console.log('🦊 Auth API running on http://localhost:3000');
console.log('📚 Auth endpoints:');
console.log(' POST /auth/register - Register new user');
console.log(' POST /auth/login - User login');
console.log(' POST /auth/refresh - Refresh access token');
console.log(' POST /auth/logout - User logout');
console.log(' GET /auth/profile - Get user profile (protected)');
console.log(' POST /auth/verify-email - Verify email address');
console.log(' POST /auth/forgot-password - Request password reset');
});Variables de Entorno
# .env
JWT_SECRET=your-super-secret-jwt-key-at-least-32-characters
JWT_REFRESH_SECRET=your-refresh-secret-key-different-from-access
NODE_ENV=development
# SMTP Configuration
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@yourapp.com
# App Configuration
APP_NAME=Fox Framework App
FRONTEND_URL=http://localhost:3001Cliente Frontend
// auth-client.ts
class AuthClient {
private baseUrl: string;
private accessToken?: string;
constructor(baseUrl: string) {
this.baseUrl = baseUrl;
this.loadTokenFromStorage();
}
private loadTokenFromStorage() {
this.accessToken = localStorage.getItem('accessToken') || undefined;
}
private saveTokenToStorage(token: string) {
this.accessToken = token;
localStorage.setItem('accessToken', token);
}
private removeTokenFromStorage() {
this.accessToken = undefined;
localStorage.removeItem('accessToken');
}
async register(userData: {
email: string;
password: string;
firstName: string;
lastName: string;
}) {
const response = await fetch(`${this.baseUrl}/auth/register`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(userData),
});
return response.json();
}
async login(email: string, password: string) {
const response = await fetch(`${this.baseUrl}/auth/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
credentials: 'include', // Para incluir cookies
body: JSON.stringify({ email, password }),
});
const data = await response.json();
if (response.ok && data.data?.accessToken) {
this.saveTokenToStorage(data.data.accessToken);
}
return data;
}
async logout() {
await fetch(`${this.baseUrl}/auth/logout`, {
method: 'POST',
credentials: 'include',
headers: {
...(this.accessToken && { Authorization: `Bearer ${this.accessToken}` }),
},
});
this.removeTokenFromStorage();
}
async getProfile() {
const response = await fetch(`${this.baseUrl}/auth/profile`, {
headers: {
Authorization: `Bearer ${this.accessToken}`,
},
});
return response.json();
}
async refreshToken() {
const response = await fetch(`${this.baseUrl}/auth/refresh`, {
method: 'POST',
credentials: 'include',
});
const data = await response.json();
if (response.ok && data.data?.accessToken) {
this.saveTokenToStorage(data.data.accessToken);
}
return data;
}
isAuthenticated(): boolean {
return !!this.accessToken;
}
getToken(): string | undefined {
return this.accessToken;
}
}
// Uso del cliente
const authClient = new AuthClient('http://localhost:3000');
// Registro
authClient.register({
email: 'user@example.com',
password: 'securePassword123',
firstName: 'John',
lastName: 'Doe'
}).then(response => {
console.log('User registered:', response);
});
// Login
authClient.login('user@example.com', 'securePassword123')
.then(response => {
if (response.data?.accessToken) {
console.log('Login successful');
// Redirigir al dashboard
}
});Características Destacadas
- JWT con Refresh Tokens: Sistema completo de tokens de acceso y renovación
- Integración SMTP: Envío de emails de verificación y recuperación de contraseña
- Seguridad robusta: Hashing de contraseñas, rate limiting, validaciones
- Gestión de sesiones: Cookies httpOnly para refresh tokens
- Middleware de autenticación: Protección flexible de rutas
- Emails HTML: Templates atractivos para notificaciones por email
- Cliente TypeScript: Cliente completo con manejo de tokens automático