Autenticación JWT
Wabot incluye un sistema de autenticación JWT que funciona tanto en controladores REST como en controladores Socket. Soporta access tokens de corta duración y refresh tokens de larga duración.
Conceptos clave
JwtConfig: singleton que lee las variables de entorno JWT automáticamente.@middleware(JwtGuardMiddleware): protege endpoints REST exigiendo unAuthorization: Bearer <token>header.@handshakeMiddlewares([JwtHandshakeGuardMiddleware]): protege controladores socket exigiendoauth.tokenen el handshake.Auth<D>: servicio scoped que almacena la información del usuario autenticado para el request/conexión actual.Jwt: servicio para crear tokens (access + refresh).
Paso 1: Variables de entorno
Configura las siguientes variables en tu archivo .env:
JWT_SECRET=tu-clave-secreta-muy-larga-y-seguraJWT_ALGORITHM=HS256 # Opcional, default: HS256JWT_ACCESS_EXPIRATION_SECONDS=600 # Opcional, default: 600 (10 min)JWT_REFRESH_EXPIRATION_SECONDS=31536000 # Opcional, default: 31536000 (1 año)JwtConfig es un singleton que lee estas variables automáticamente al iniciar:
import { JwtConfig, singleton, Env } from '@wabot-dev/framework'
// JwtConfig se resuelve automáticamente — no necesitas instanciarlo.// Internamente hace:@singleton()export class JwtConfig { secretOrPublicKey: string algorithm: Algorithm accessExpirationSeconds: number refreshExpirationSeconds: number
constructor(env: Env) { this.algorithm = env.requireString('JWT_ALGORITHM', { default: 'HS256' }) this.secretOrPublicKey = env.requireString('JWT_SECRET') this.accessExpirationSeconds = env.requireNumber('JWT_ACCESS_EXPIRATION_SECONDS', { default: 600 }) this.refreshExpirationSeconds = env.requireNumber('JWT_REFRESH_EXPIRATION_SECONDS', { default: 31536000 }) }}Paso 2: Auth — almacenar info del usuario
Auth<D> es un servicio scoped (por request/conexión) que permite almacenar y recuperar la información del usuario autenticado:
import { Auth } from '@wabot-dev/framework'
// Definir el tipo de la información de autenticacióninterface UserAuthInfo { userId: string role: 'admin' | 'user' email: string}
// En un controlador o servicio:constructor(private auth: Auth<UserAuthInfo>) {}
// Asignar info después de autenticarthis.auth.assign({ userId: '123', role: 'admin', email: 'user@example.com' })
// Recuperar info (lanza error si no hay usuario autenticado)const user = this.auth.require()console.log(user.userId) // '123'Paso 3: Crear tokens
El servicio Jwt genera pares de tokens (access + refresh):
import { Jwt, JwtAccessAndRefreshTokenDto } from '@wabot-dev/framework'
// En un controlador de login:constructor( private jwt: Jwt, private auth: Auth<UserAuthInfo>,) {}
async login(credentials: LoginRequest) { // 1. Validar credenciales (tu lógica) const user = await this.userService.authenticate(credentials.email, credentials.password)
// 2. Asignar info de auth this.auth.assign({ userId: user.id, role: user.role, email: user.email })
// 3. Crear tokens const tokens: JwtAccessAndRefreshTokenDto = await this.jwt.createToken() return tokens // Retorna: // { // access: { token: 'eyJ...', expiration: Date }, // refresh: { token: 'rt_abc123...', expiration: Date } // }}Paso 4: Proteger endpoints REST
Usa JwtGuardMiddleware como middleware en endpoints que requieren autenticación:
import { restController, onGet, onPost, middleware } from '@wabot-dev/framework'import { JwtGuardMiddleware } from '@wabot-dev/framework'
@restController('/api/profile')export class ProfileController { constructor( private auth: Auth<UserAuthInfo>, private userService: UserService, ) {}
@onGet() @middleware(JwtGuardMiddleware) async getProfile() { const userInfo = this.auth.require() return await this.userService.findById(userInfo.userId) }
@onPost('/update') @middleware(JwtGuardMiddleware) async updateProfile(req: UpdateProfileRequest) { const userInfo = this.auth.require() return await this.userService.update(userInfo.userId, req) }}El cliente debe enviar el token en el header Authorization:
curl -H "Authorization: Bearer eyJ..." http://localhost:3000/api/profilePaso 5: Proteger controladores Socket
Usa JwtHandshakeGuardMiddleware para autenticar conexiones socket durante el handshake:
import { socketController, handshakeMiddlewares, onSocketEvent } from '@wabot-dev/framework'import { JwtHandshakeGuardMiddleware } from '@wabot-dev/framework'
@socketController('/protected')@handshakeMiddlewares([JwtHandshakeGuardMiddleware])export class ProtectedController { constructor(private auth: Auth<UserAuthInfo>) {}
@onSocketEvent('get-data') async getData(req: DataRequest, socket: Socket, callback: Function) { const user = this.auth.require() // El usuario ya está autenticado por el middleware de handshake callback({ userId: user.userId, data: '...' }) }}El cliente debe enviar el token en auth.token durante la conexión:
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000/protected', { auth: { token: 'eyJ...', // El access token JWT },})Paso 6: Refresh tokens
Cuando el access token expira, el cliente usa el refresh token para obtener nuevos tokens:
@restController('/api/auth')export class AuthController { constructor( private jwt: Jwt, private auth: Auth<UserAuthInfo>, private userService: UserService, ) {}
@onPost('/login') async login(req: LoginRequest) { const user = await this.userService.authenticate(req.email!, req.password!) this.auth.assign({ userId: user.id, role: user.role, email: user.email }) return await this.jwt.createToken() }
@onPost('/refresh') async refresh(req: RefreshTokenRequest) { // Buscar la info de auth asociada al refresh token const authInfo = await this.jwt.findRefreshTokenAuthInfo(req.refreshToken!) if (!authInfo) { throw new Error('Refresh token inválido o expirado') }
// Asignar la info y generar nuevos tokens this.auth.assign(authInfo) return await this.jwt.createToken() }}Ejemplo completo
Login + endpoint protegido REST + controlador socket protegido:
import { restController, onGet, onPost, middleware, socketController, handshakeMiddlewares, onSocketEvent, isString, isNotEmpty, injectable, Auth, Jwt, runRestControllers, runSocketControllers,} from '@wabot-dev/framework'import { JwtGuardMiddleware, JwtHandshakeGuardMiddleware } from '@wabot-dev/framework'import type { Socket } from 'socket.io'
// --- Tipo de auth ---
interface UserAuthInfo { userId: string email: string role: string}
// --- Requests ---
export class LoginRequest { @isString() @isNotEmpty() email?: string
@isString() @isNotEmpty() password?: string}
export class RefreshTokenRequest { @isString() @isNotEmpty() refreshToken?: string}
// --- Auth Controller (REST) ---
@restController('/api/auth')export class AuthController { constructor( private jwt: Jwt, private auth: Auth<UserAuthInfo>, ) {}
@onPost('/login') async login(req: LoginRequest) { // Aquí validas credenciales contra tu base de datos const user = { userId: '1', email: req.email!, role: 'admin' } this.auth.assign(user) return await this.jwt.createToken() }
@onPost('/refresh') async refresh(req: RefreshTokenRequest) { const authInfo = await this.jwt.findRefreshTokenAuthInfo(req.refreshToken!) if (!authInfo) throw new Error('Token inválido') this.auth.assign(authInfo) return await this.jwt.createToken() }
@onGet('/me') @middleware(JwtGuardMiddleware) async me() { return this.auth.require() }}
// --- Dashboard Controller (Socket protegido) ---
@socketController('/dashboard')@handshakeMiddlewares([JwtHandshakeGuardMiddleware])export class DashboardController { constructor(private auth: Auth<UserAuthInfo>) {}
@onSocketEvent('get-stats') async getStats(req: any, socket: Socket, callback: Function) { const user = this.auth.require() callback({ userId: user.userId, stats: { visitors: 1234, sales: 56 }, }) }}
// --- Registro ---runRestControllers([AuthController])runSocketControllers([DashboardController])Cliente REST
// Loginconst loginResponse = await fetch('http://localhost:3000/api/auth/login', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email: 'user@example.com', password: 'secret' }),})const tokens = await loginResponse.json()
// Usar endpoint protegidoconst meResponse = await fetch('http://localhost:3000/api/auth/me', { headers: { Authorization: `Bearer ${tokens.access.token}` },})const user = await meResponse.json()
// Refresh cuando expireconst refreshResponse = await fetch('http://localhost:3000/api/auth/refresh', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ refreshToken: tokens.refresh.token }),})const newTokens = await refreshResponse.json()Cliente Socket
import { io } from 'socket.io-client'
const socket = io('http://localhost:3000/dashboard', { auth: { token: tokens.access.token },})
socket.on('connect', () => { socket.emit('get-stats', {}, (response) => { console.log('Stats:', response.stats) })})
socket.on('connect_error', (error) => { if (error.message === 'Unauthorized') { // Refrescar token y reconectar socket.auth = { token: newAccessToken } socket.connect() }})Siguiente paso
Ejecuta tareas en background con comandos asíncronos: Comandos Asíncronos.