Skip to content

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 un Authorization: Bearer <token> header.
  • @handshakeMiddlewares([JwtHandshakeGuardMiddleware]): protege controladores socket exigiendo auth.token en 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:

Terminal window
JWT_SECRET=tu-clave-secreta-muy-larga-y-segura
JWT_ALGORITHM=HS256 # Opcional, default: HS256
JWT_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ón
interface UserAuthInfo {
userId: string
role: 'admin' | 'user'
email: string
}
// En un controlador o servicio:
constructor(private auth: Auth<UserAuthInfo>) {}
// Asignar info después de autenticar
this.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:

Terminal window
curl -H "Authorization: Bearer eyJ..." http://localhost:3000/api/profile

Paso 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

// Login
const 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 protegido
const meResponse = await fetch('http://localhost:3000/api/auth/me', {
headers: { Authorization: `Bearer ${tokens.access.token}` },
})
const user = await meResponse.json()
// Refresh cuando expire
const 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.