Saltearse al contenido

Autenticación y Autorización

Aprende a implementar autenticación segura y control de acceso basado en roles (RBAC) en tus aplicaciones Veloce-TS.

Veloce-TS incluye patrones y plugins para autenticación y autorización (confirma la API con tu versión instalada y el registro de cambios):

  • Autenticación basada en JWT con tokens de acceso y actualización
  • Control de Acceso Basado en Roles (RBAC) con roles jerárquicos
  • Autorización basada en Permisos para control de acceso granular
  • Gestión de Usuarios con hash de contraseñas y validación
  • Gestión de Sesiones (opcional)
import { Veloce, AuthPlugin } from 'veloce-ts';
import bcrypt from 'bcrypt';
const app = new Veloce();
app.usePlugin(new AuthPlugin({
jwt: {
secret: process.env.JWT_SECRET || 'tu-clave-secreta',
expiresIn: '1h',
refreshExpiresIn: '7d',
},
userProvider: {
findByCredentials: async (username: string, password: string) => {
// Buscar usuario y verificar contraseña
const user = await userService.findByUsername(username);
if (!user) return null;
const isValid = await bcrypt.compare(password, user.password);
return isValid ? user : null;
},
findById: async (id: string) => {
return await userService.findById(id);
},
hashPassword: async (password: string) => {
return await bcrypt.hash(password, 10);
},
verifyPassword: async (password: string, hash: string) => {
return await bcrypt.compare(password, hash);
}
}
}));
import {
Controller,
Post,
Get,
Body,
Auth,
CurrentUser
} from 'veloce-ts';
import { z } from 'zod';
const LoginSchema = z.object({
username: z.string().min(1),
password: z.string().min(6)
});
const RegisterSchema = z.object({
username: z.string().min(3).max(50),
email: z.string().email(),
password: z.string().min(6)
});
@Controller('/auth')
export class AuthController {
@Post('/login')
async login(@Body(LoginSchema) credentials: z.infer<typeof LoginSchema>) {
const user = await userService.validateCredentials(
credentials.username,
credentials.password
);
if (!user) {
throw new UnauthorizedException('Credenciales inválidas');
}
const tokens = authService.generateTokens({
sub: user.id,
username: user.username,
email: user.email,
role: user.role,
permissions: user.permissions
});
return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
};
}
@Post('/register')
async register(@Body(RegisterSchema) userData: z.infer<typeof RegisterSchema>) {
const existingUser = await userService.findByUsername(userData.username);
if (existingUser) {
throw new ConflictException('El nombre de usuario ya existe');
}
const hashedPassword = await userService.hashPassword(userData.password);
const user = await userService.create({
...userData,
password: hashedPassword,
role: 'viewer', // Rol por defecto
permissions: ['tasks.read'] // Permisos por defecto
});
return {
message: 'Usuario creado exitosamente',
user: {
id: user.id,
username: user.username,
email: user.email,
role: user.role
}
};
}
@Post('/refresh')
async refresh(@Body() body: { refreshToken: string }) {
try {
const tokens = await authService.refreshToken(body.refreshToken);
return tokens;
} catch (error) {
throw new UnauthorizedException('Token de actualización inválido');
}
}
@Get('/me')
@Auth()
async getProfile(@CurrentUser() user: any) {
return {
id: user.id,
username: user.username,
email: user.email,
role: user.role,
permissions: user.permissions
};
}
@Post('/logout')
@Auth()
async logout(@CurrentUser() user: any) {
// Invalidar token de actualización
await authService.invalidateRefreshToken(user.id);
return { message: 'Sesión cerrada exitosamente' };
}
}
import { RBACPlugin } from 'veloce-ts/auth';
// Debe instalarse después de AuthPlugin
app.usePlugin(new RBACPlugin({
roles: [
{ name: 'admin', level: 100, permissions: ['users.*', 'tasks.*', 'admin.*'] },
{ name: 'manager', level: 80, permissions: ['tasks.*', 'teams.*'] },
{ name: 'team_lead', level: 60, permissions: ['tasks.create', 'tasks.read', 'tasks.update'] },
{ name: 'developer', level: 40, permissions: ['tasks.create', 'tasks.read', 'tasks.update'] },
{ name: 'viewer', level: 20, permissions: ['tasks.read', 'users.read'] },
],
}));

Los roles son jerárquicos basados en su nivel:

  • Admin (100): Acceso completo a todo
  • Manager (80): Puede gestionar equipos y usuarios
  • Team Lead (60): Puede gestionar tareas del equipo
  • Developer (40): Puede crear y actualizar tareas
  • Viewer (20): Acceso de solo lectura
// Requerir autenticación
@Get('/protected')
@Auth()
async protectedRoute(@CurrentUser() user: any) {
return { message: 'Esta ruta está protegida', user: user.username };
}
// Requerir nivel mínimo de rol
@Get('/admin-only')
@MinimumRole('admin')
async adminOnly() {
return { message: 'Acceso de administrador concedido' };
}
@Get('/manager-up')
@MinimumRole('manager')
async managerUp() {
return { message: 'Acceso de nivel manager concedido' };
}
// Permiso único (pasar como array)
@Post('/users')
@Permissions(['users.create'])
async createUser(@Body() userData: any) {
return await userService.create(userData);
}
// Múltiples permisos — el usuario necesita CUALQUIERA (por defecto)
@Put('/users/:id')
@Permissions(['users.update', 'users.read'])
async updateUser(@Param('id') id: string, @Body() userData: any) {
return await userService.update(id, userData);
}
// Requerir TODOS los permisos + rol mínimo
@Delete('/users/:id')
@Permissions({ permissions: ['users.delete'], requireAll: true })
@MinimumRole('manager')
async deleteUser(@Param('id') id: string) {
return await userService.delete(id);
}
import { Injectable } from 'veloce-ts';
import bcrypt from 'bcrypt';
@Injectable('singleton')
export class UserService {
async findByUsername(username: string): Promise<User | null> {
// Tu lógica de consulta a base de datos
return await this.db.query('SELECT * FROM users WHERE username = ?', [username]);
}
async findById(id: string): Promise<User | null> {
return await this.db.query('SELECT * FROM users WHERE id = ?', [id]);
}
async validateCredentials(username: string, password: string): Promise<User | null> {
const user = await this.findByUsername(username);
if (!user) return null;
const isValid = await this.verifyPassword(password, user.password);
return isValid ? user : null;
}
async hashPassword(password: string): Promise<string> {
return await bcrypt.hash(password, 10);
}
async verifyPassword(password: string, hash: string): Promise<boolean> {
return await bcrypt.compare(password, hash);
}
async create(userData: CreateUserDto): Promise<User> {
const hashedPassword = await this.hashPassword(userData.password);
const user = {
...userData,
password: hashedPassword,
id: generateId(),
createdAt: new Date(),
updatedAt: new Date()
};
await this.db.query(
'INSERT INTO users (id, username, email, password, role, permissions) VALUES (?, ?, ?, ?, ?, ?)',
[user.id, user.username, user.email, user.password, user.role, JSON.stringify(user.permissions)]
);
return user;
}
}
.env
JWT_SECRET=tu-clave-secreta-jwt-super-segura-aqui
JWT_ACCESS_TTL=3600
JWT_REFRESH_TTL=86400
BCRYPT_ROUNDS=12
// Usar requisitos de contraseña fuertes
const PasswordSchema = z.string()
.min(8, 'La contraseña debe tener al menos 8 caracteres')
.regex(/[A-Z]/, 'La contraseña debe contener letra mayúscula')
.regex(/[a-z]/, 'La contraseña debe contener letra minúscula')
.regex(/[0-9]/, 'La contraseña debe contener número')
.regex(/[^A-Za-z0-9]/, 'La contraseña debe contener carácter especial');
// Implementar lista negra de tokens
@Injectable('singleton')
export class TokenService {
private blacklistedTokens = new Set<string>();
blacklistToken(tokenId: string) {
this.blacklistedTokens.add(tokenId);
}
isTokenBlacklisted(tokenId: string): boolean {
return this.blacklistedTokens.has(tokenId);
}
}

Usa el decorador @RateLimit por ruta o createRateLimitMiddleware para middleware global:

import { Post, RateLimit } from 'veloce-ts';
// Limitación de velocidad declarativa por ruta
@Post('/login')
@RateLimit({ windowMs: 15 * 60 * 1000, max: 5 })
async login(@Body(LoginSchema) credentials: any) {
// ...
}
// 1. Registrar usuario
POST /auth/register
{
"username": "johndoe",
"email": "john@ejemplo.com",
"password": "SecurePass123!"
}
// 2. Iniciar sesión
POST /auth/login
{
"username": "johndoe",
"password": "SecurePass123!"
}
// Respuesta:
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": "123",
"username": "johndoe",
"email": "john@ejemplo.com",
"role": "viewer"
}
}
// 3. Usar endpoint protegido
GET /auth/me
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
// 4. Usar endpoint protegido por rol
GET /admin/dashboard
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
@Controller('/dashboard')
export class DashboardController {
@Get('/')
@Auth()
async getDashboard(@CurrentUser() user: any) {
const baseDashboard = {
user: {
id: user.id,
username: user.username,
role: user.role
}
};
// Añadir datos específicos por rol
if (user.role === 'admin') {
return {
...baseDashboard,
adminStats: await this.getAdminStats(),
systemHealth: await this.getSystemHealth()
};
}
if (user.role === 'manager') {
return {
...baseDashboard,
teamStats: await this.getTeamStats(user.id),
pendingApprovals: await this.getPendingApprovals(user.id)
};
}
return {
...baseDashboard,
myTasks: await this.getUserTasks(user.id),
notifications: await this.getUserNotifications(user.id)
};
}
@Get('/admin-stats')
@MinimumRole('admin')
async getAdminStats() {
return {
totalUsers: await this.userService.count(),
totalTasks: await this.taskService.count(),
systemUptime: process.uptime()
};
}
}