La autenticacion (Authentication) y la autorizacion (Authorization) son la base de la seguridad de las aplicaciones. En este articulo explicamos el diseno e implementacion de patrones de autenticacion y autorizacion en aplicaciones web modernas.
Fundamentos de Autenticacion y Autorizacion
Diferencia entre Conceptos
Autenticacion (Authentication) - “Quien eres?”
sequenceDiagram
participant U as Usuario
participant S as Sistema
U->>S: Presentacion de credenciales (ID/Contrasena)
S->>U: Resultado de verificacion de identidad (Token de autenticacion)
Autorizacion (Authorization) - “Que puedes hacer?”
sequenceDiagram
participant U as Usuario
participant S as Sistema
U->>S: Solicitud de recurso (+ Token de autenticacion)
S->>S: Verificacion de permisos
S->>U: Permitido/Denegado
Comparacion de Metodos de Autenticacion
| Metodo | Caracteristicas | Caso de Uso |
|---|---|---|
| Basado en Sesion | Gestion de estado en el servidor | Aplicaciones web tradicionales |
| JWT (Sin estado) | Informacion contenida en el token | SPA, API |
| OAuth 2.0 / OIDC | Integracion con proveedores de autenticacion externos | Login social |
| API Key | Clave estatica | Comunicacion entre servidores |
| mTLS | Basado en certificados | Entre microservicios |
Autenticacion Basada en Sesion
Ejemplo de Implementacion
// session-auth.ts
import { randomBytes, createHash } from 'crypto';
import Redis from 'ioredis';
interface Session {
userId: string;
email: string;
role: string;
createdAt: number;
lastAccessedAt: number;
userAgent: string;
ip: string;
}
class SessionManager {
private redis: Redis;
private readonly SESSION_TTL = 24 * 60 * 60; // 24 horas
private readonly SESSION_PREFIX = 'session:';
constructor(redisUrl: string) {
this.redis = new Redis(redisUrl);
}
async createSession(userId: string, userData: Partial<Session>, req: Request): Promise<string> {
const sessionId = this.generateSessionId();
const hashedId = this.hashSessionId(sessionId);
const session: Session = {
userId,
email: userData.email || '',
role: userData.role || 'user',
createdAt: Date.now(),
lastAccessedAt: Date.now(),
userAgent: req.headers.get('user-agent') || '',
ip: req.headers.get('x-forwarded-for') || '',
};
await this.redis.setex(
`${this.SESSION_PREFIX}${hashedId}`,
this.SESSION_TTL,
JSON.stringify(session)
);
// Gestionar lista de sesiones del usuario
await this.redis.sadd(`user_sessions:${userId}`, hashedId);
return sessionId;
}
async getSession(sessionId: string): Promise<Session | null> {
const hashedId = this.hashSessionId(sessionId);
const data = await this.redis.get(`${this.SESSION_PREFIX}${hashedId}`);
if (!data) return null;
const session: Session = JSON.parse(data);
// Actualizar hora del ultimo acceso
session.lastAccessedAt = Date.now();
await this.redis.setex(
`${this.SESSION_PREFIX}${hashedId}`,
this.SESSION_TTL,
JSON.stringify(session)
);
return session;
}
async destroySession(sessionId: string): Promise<void> {
const hashedId = this.hashSessionId(sessionId);
const data = await this.redis.get(`${this.SESSION_PREFIX}${hashedId}`);
if (data) {
const session: Session = JSON.parse(data);
await this.redis.srem(`user_sessions:${session.userId}`, hashedId);
}
await this.redis.del(`${this.SESSION_PREFIX}${hashedId}`);
}
async destroyAllUserSessions(userId: string): Promise<void> {
const sessionIds = await this.redis.smembers(`user_sessions:${userId}`);
for (const hashedId of sessionIds) {
await this.redis.del(`${this.SESSION_PREFIX}${hashedId}`);
}
await this.redis.del(`user_sessions:${userId}`);
}
private generateSessionId(): string {
return randomBytes(32).toString('hex');
}
private hashSessionId(sessionId: string): string {
return createHash('sha256').update(sessionId).digest('hex');
}
}
// Configuracion de Cookie
function setSessionCookie(response: Response, sessionId: string): Response {
const cookie = [
`session=${sessionId}`,
'HttpOnly',
'Secure',
'SameSite=Strict',
'Path=/',
`Max-Age=${24 * 60 * 60}`,
].join('; ');
response.headers.set('Set-Cookie', cookie);
return response;
}
Autenticacion Basada en JWT
Implementacion de JWT
// jwt-auth.ts
import { SignJWT, jwtVerify, JWTPayload } from 'jose';
interface TokenPayload extends JWTPayload {
sub: string; // userId
email: string;
role: string;
type: 'access' | 'refresh';
}
interface TokenPair {
accessToken: string;
refreshToken: string;
}
class JWTManager {
private readonly accessSecret: Uint8Array;
private readonly refreshSecret: Uint8Array;
private readonly issuer = 'myapp';
private readonly audience = 'myapp-users';
private readonly ACCESS_TOKEN_TTL = '15m';
private readonly REFRESH_TOKEN_TTL = '7d';
constructor(accessSecret: string, refreshSecret: string) {
this.accessSecret = new TextEncoder().encode(accessSecret);
this.refreshSecret = new TextEncoder().encode(refreshSecret);
}
async generateTokenPair(userId: string, userData: { email: string; role: string }): Promise<TokenPair> {
const now = new Date();
const accessToken = await new SignJWT({
sub: userId,
email: userData.email,
role: userData.role,
type: 'access',
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(this.issuer)
.setAudience(this.audience)
.setExpirationTime(this.ACCESS_TOKEN_TTL)
.sign(this.accessSecret);
const refreshToken = await new SignJWT({
sub: userId,
type: 'refresh',
})
.setProtectedHeader({ alg: 'HS256', typ: 'JWT' })
.setIssuedAt()
.setIssuer(this.issuer)
.setAudience(this.audience)
.setExpirationTime(this.REFRESH_TOKEN_TTL)
.sign(this.refreshSecret);
return { accessToken, refreshToken };
}
async verifyAccessToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.accessSecret, {
issuer: this.issuer,
audience: this.audience,
});
if (payload.type !== 'access') {
return null;
}
return payload as TokenPayload;
} catch (error) {
return null;
}
}
async verifyRefreshToken(token: string): Promise<TokenPayload | null> {
try {
const { payload } = await jwtVerify(token, this.refreshSecret, {
issuer: this.issuer,
audience: this.audience,
});
if (payload.type !== 'refresh') {
return null;
}
return payload as TokenPayload;
} catch (error) {
return null;
}
}
async refreshTokens(
refreshToken: string,
getUserData: (userId: string) => Promise<{ email: string; role: string } | null>
): Promise<TokenPair | null> {
const payload = await this.verifyRefreshToken(refreshToken);
if (!payload || !payload.sub) {
return null;
}
const userData = await getUserData(payload.sub);
if (!userData) {
return null;
}
return this.generateTokenPair(payload.sub, userData);
}
}
// Middleware
async function authMiddleware(
request: Request,
jwtManager: JWTManager
): Promise<TokenPayload | Response> {
const authHeader = request.headers.get('Authorization');
if (!authHeader?.startsWith('Bearer ')) {
return new Response('Unauthorized', { status: 401 });
}
const token = authHeader.slice(7);
const payload = await jwtManager.verifyAccessToken(token);
if (!payload) {
return new Response('Invalid token', { status: 401 });
}
return payload;
}
Flujo de Renovacion de Token
sequenceDiagram
participant C as Client
participant S as Server
rect rgb(240, 248, 255)
Note over C,S: 1. Login inicial
C->>S: POST /auth/login<br/>{email, password}
S->>C: 200 OK<br/>{accessToken, refreshToken}
end
rect rgb(245, 255, 245)
Note over C,S: 2. Llamada a API
C->>S: GET /api/resource<br/>Authorization: Bearer {accessToken}
S->>C: 200 OK / 401 Unauthorized
end
rect rgb(255, 250, 240)
Note over C,S: 3. Renovacion de token (cuando accessToken expira)
C->>S: POST /auth/refresh<br/>{refreshToken}
S->>C: 200 OK<br/>{accessToken, refreshToken} (ambos tokens nuevos)
end
Patrones de Autorizacion
RBAC (Control de Acceso Basado en Roles)
// rbac.ts
type Permission =
| 'read:users' | 'write:users' | 'delete:users'
| 'read:posts' | 'write:posts' | 'delete:posts'
| 'admin:*';
type Role = 'guest' | 'user' | 'moderator' | 'admin';
const rolePermissions: Record<Role, Permission[]> = {
guest: ['read:posts'],
user: ['read:posts', 'write:posts', 'read:users'],
moderator: ['read:posts', 'write:posts', 'delete:posts', 'read:users'],
admin: ['admin:*'],
};
class RBACAuthorizer {
hasPermission(role: Role, permission: Permission): boolean {
const permissions = rolePermissions[role];
if (permissions.includes('admin:*')) {
return true;
}
return permissions.includes(permission);
}
hasAnyPermission(role: Role, permissions: Permission[]): boolean {
return permissions.some(p => this.hasPermission(role, p));
}
hasAllPermissions(role: Role, permissions: Permission[]): boolean {
return permissions.every(p => this.hasPermission(role, p));
}
}
// Middleware
function requirePermission(...permissions: Permission[]) {
return async function (request: Request, user: TokenPayload): Promise<Response | null> {
const authorizer = new RBACAuthorizer();
if (!authorizer.hasAllPermissions(user.role as Role, permissions)) {
return new Response('Forbidden', { status: 403 });
}
return null; // Permitido
};
}
// Ejemplo de uso
app.delete('/api/posts/:id', async (req) => {
const user = await authMiddleware(req, jwtManager);
if (user instanceof Response) return user;
const forbidden = await requirePermission('delete:posts')(req, user);
if (forbidden) return forbidden;
// Proceso de eliminacion...
});
ABAC (Control de Acceso Basado en Atributos)
// abac.ts
interface Resource {
type: string;
ownerId: string;
attributes: Record<string, any>;
}
interface Subject {
id: string;
role: string;
department: string;
attributes: Record<string, any>;
}
interface Context {
time: Date;
ip: string;
location?: string;
}
type PolicyCondition = (subject: Subject, resource: Resource, context: Context) => boolean;
interface Policy {
name: string;
effect: 'allow' | 'deny';
actions: string[];
resources: string[];
condition: PolicyCondition;
}
class ABACAuthorizer {
private policies: Policy[] = [];
addPolicy(policy: Policy): void {
this.policies.push(policy);
}
evaluate(
action: string,
subject: Subject,
resource: Resource,
context: Context
): boolean {
// Buscar politicas que coincidan
const matchingPolicies = this.policies.filter(policy => {
const actionMatch = policy.actions.includes(action) || policy.actions.includes('*');
const resourceMatch = policy.resources.includes(resource.type) || policy.resources.includes('*');
return actionMatch && resourceMatch;
});
// Denegacion por defecto
if (matchingPolicies.length === 0) {
return false;
}
// Evaluar condiciones
for (const policy of matchingPolicies) {
const conditionResult = policy.condition(subject, resource, context);
if (policy.effect === 'deny' && conditionResult) {
return false;
}
if (policy.effect === 'allow' && conditionResult) {
return true;
}
}
return false;
}
}
// Definicion de politicas
const authorizer = new ABACAuthorizer();
// Puede editar sus propios recursos
authorizer.addPolicy({
name: 'owner-can-edit',
effect: 'allow',
actions: ['update', 'delete'],
resources: ['post', 'comment'],
condition: (subject, resource) => subject.id === resource.ownerId,
});
// El administrador puede hacer todo
authorizer.addPolicy({
name: 'admin-full-access',
effect: 'allow',
actions: ['*'],
resources: ['*'],
condition: (subject) => subject.role === 'admin',
});
// Puede ver recursos del mismo departamento
authorizer.addPolicy({
name: 'same-department-read',
effect: 'allow',
actions: ['read'],
resources: ['document'],
condition: (subject, resource) =>
subject.department === resource.attributes.department,
});
// Acceso restringido fuera del horario laboral
authorizer.addPolicy({
name: 'business-hours-only',
effect: 'deny',
actions: ['*'],
resources: ['sensitive-data'],
condition: (_, __, context) => {
const hour = context.time.getHours();
return hour < 9 || hour >= 18; // Denegar fuera de 9-18h
},
});
Control de Acceso Basado en Recursos
// resource-based-auth.ts
interface Post {
id: string;
authorId: string;
status: 'draft' | 'published' | 'archived';
visibility: 'public' | 'private' | 'members-only';
}
class PostAuthorizer {
canRead(user: Subject | null, post: Post): boolean {
// Las publicaciones publicas pueden ser vistas por cualquiera
if (post.status === 'published' && post.visibility === 'public') {
return true;
}
// Usuarios no autenticados solo pueden ver publicaciones publicas
if (!user) {
return false;
}
// Siempre puede ver sus propias publicaciones
if (post.authorId === user.id) {
return true;
}
// Los administradores pueden ver todo
if (user.role === 'admin' || user.role === 'moderator') {
return true;
}
// Publicaciones solo para miembros
if (post.visibility === 'members-only' && post.status === 'published') {
return true;
}
return false;
}
canUpdate(user: Subject, post: Post): boolean {
// Solo el autor puede editar
if (post.authorId === user.id) {
return true;
}
// Los administradores tambien pueden editar
if (user.role === 'admin') {
return true;
}
return false;
}
canDelete(user: Subject, post: Post): boolean {
// Solo el autor puede eliminar (solo borradores)
if (post.authorId === user.id && post.status === 'draft') {
return true;
}
// Los administradores pueden eliminar todo
if (user.role === 'admin') {
return true;
}
return false;
}
canPublish(user: Subject, post: Post): boolean {
// Solo el autor puede publicar
if (post.authorId === user.id) {
return true;
}
// Los administradores tambien pueden publicar
if (user.role === 'admin') {
return true;
}
return false;
}
}
// Ejemplo de uso
const postAuth = new PostAuthorizer();
app.get('/api/posts/:id', async (req, { params }) => {
const user = await getOptionalUser(req); // Puede ser null
const post = await getPost(params.id);
if (!post) {
return new Response('Not Found', { status: 404 });
}
if (!postAuth.canRead(user, post)) {
return new Response('Forbidden', { status: 403 });
}
return Response.json(post);
});
Autenticacion Multifactor (MFA)
Implementacion de TOTP
// mfa.ts
import { createHmac, randomBytes } from 'crypto';
class TOTPManager {
private readonly DIGITS = 6;
private readonly PERIOD = 30; // segundos
private readonly ALGORITHM = 'sha1';
generateSecret(): string {
return randomBytes(20).toString('base32').slice(0, 16);
}
generateOTP(secret: string, counter?: number): string {
const time = counter ?? Math.floor(Date.now() / 1000 / this.PERIOD);
const timeBuffer = Buffer.alloc(8);
timeBuffer.writeBigInt64BE(BigInt(time));
const decodedSecret = this.base32Decode(secret);
const hmac = createHmac(this.ALGORITHM, decodedSecret)
.update(timeBuffer)
.digest();
const offset = hmac[hmac.length - 1] & 0xf;
const binary =
((hmac[offset] & 0x7f) << 24) |
((hmac[offset + 1] & 0xff) << 16) |
((hmac[offset + 2] & 0xff) << 8) |
(hmac[offset + 3] & 0xff);
const otp = binary % Math.pow(10, this.DIGITS);
return otp.toString().padStart(this.DIGITS, '0');
}
verifyOTP(secret: string, otp: string, window: number = 1): boolean {
const currentCounter = Math.floor(Date.now() / 1000 / this.PERIOD);
for (let i = -window; i <= window; i++) {
const expectedOTP = this.generateOTP(secret, currentCounter + i);
if (this.timingSafeEqual(otp, expectedOTP)) {
return true;
}
}
return false;
}
generateQRCodeURL(secret: string, email: string, issuer: string): string {
const encodedIssuer = encodeURIComponent(issuer);
const encodedEmail = encodeURIComponent(email);
return `otpauth://totp/${encodedIssuer}:${encodedEmail}?secret=${secret}&issuer=${encodedIssuer}&algorithm=SHA1&digits=${this.DIGITS}&period=${this.PERIOD}`;
}
private base32Decode(input: string): Buffer {
const alphabet = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
let bits = '';
for (const char of input.toUpperCase()) {
const val = alphabet.indexOf(char);
if (val === -1) continue;
bits += val.toString(2).padStart(5, '0');
}
const bytes: number[] = [];
for (let i = 0; i + 8 <= bits.length; i += 8) {
bytes.push(parseInt(bits.slice(i, i + 8), 2));
}
return Buffer.from(bytes);
}
private timingSafeEqual(a: string, b: string): boolean {
if (a.length !== b.length) return false;
let result = 0;
for (let i = 0; i < a.length; i++) {
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
}
return result === 0;
}
}
// Codigos de respaldo
class BackupCodeManager {
generateCodes(count: number = 10): string[] {
const codes: string[] = [];
for (let i = 0; i < count; i++) {
const code = randomBytes(4).toString('hex').toUpperCase();
codes.push(`${code.slice(0, 4)}-${code.slice(4)}`);
}
return codes;
}
hashCode(code: string): string {
return createHash('sha256')
.update(code.replace('-', ''))
.digest('hex');
}
}
Mejores Practicas de Seguridad
Politica de Contrasenas
// password-policy.ts
interface PasswordValidationResult {
valid: boolean;
errors: string[];
strength: 'weak' | 'medium' | 'strong';
}
function validatePassword(password: string): PasswordValidationResult {
const errors: string[] = [];
// Verificacion de longitud
if (password.length < 8) {
errors.push('La contrasena debe tener al menos 8 caracteres');
}
if (password.length > 128) {
errors.push('La contrasena debe tener 128 caracteres o menos');
}
// Verificacion de complejidad
if (!/[a-z]/.test(password)) {
errors.push('Debe incluir letras minusculas');
}
if (!/[A-Z]/.test(password)) {
errors.push('Debe incluir letras mayusculas');
}
if (!/[0-9]/.test(password)) {
errors.push('Debe incluir numeros');
}
if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Debe incluir simbolos');
}
// Verificacion de contrasenas comunes
const commonPasswords = ['password', '123456', 'qwerty', 'admin'];
if (commonPasswords.some(p => password.toLowerCase().includes(p))) {
errors.push('No se pueden usar contrasenas demasiado comunes');
}
// Calculo de fortaleza
let strength: 'weak' | 'medium' | 'strong' = 'weak';
if (errors.length === 0) {
if (password.length >= 12 && /[!@#$%^&*(),.?":{}|<>]/.test(password)) {
strength = 'strong';
} else {
strength = 'medium';
}
}
return {
valid: errors.length === 0,
errors,
strength,
};
}
Limitacion de Tasa
// rate-limiter.ts
class RateLimiter {
private requests: Map<string, number[]> = new Map();
constructor(
private maxRequests: number,
private windowMs: number
) {}
isAllowed(key: string): boolean {
const now = Date.now();
const windowStart = now - this.windowMs;
let timestamps = this.requests.get(key) || [];
// Eliminar solicitudes antiguas
timestamps = timestamps.filter(t => t > windowStart);
if (timestamps.length >= this.maxRequests) {
return false;
}
timestamps.push(now);
this.requests.set(key, timestamps);
return true;
}
getRemainingRequests(key: string): number {
const now = Date.now();
const windowStart = now - this.windowMs;
const timestamps = (this.requests.get(key) || []).filter(t => t > windowStart);
return Math.max(0, this.maxRequests - timestamps.length);
}
}
// Limitacion de intentos de login
const loginLimiter = new RateLimiter(5, 15 * 60 * 1000); // 5 veces en 15 minutos
app.post('/auth/login', async (req) => {
const ip = req.headers.get('x-forwarded-for') || 'unknown';
if (!loginLimiter.isAllowed(ip)) {
return new Response('Too many login attempts', {
status: 429,
headers: {
'Retry-After': '900', // 15 minutos
},
});
}
// Proceso de login...
});
Resumen
La autenticacion y autorizacion son la base de la seguridad de las aplicaciones.
Guia de Seleccion
| Requisito | Patron Recomendado |
|---|---|
| Aplicacion web tradicional | Sesion + RBAC |
| SPA/Movil | JWT + OAuth 2.0 |
| Microservicios | JWT + mTLS |
| Permisos complejos | ABAC |
| Permisos simples | RBAC |
Lista de Verificacion de Seguridad
- Contrasenas: Hashing adecuado (Argon2id/bcrypt)
- Tokens: Tiempo de expiracion y renovacion apropiados
- Sesiones: Cookies HTTPOnly, Secure, SameSite
- MFA: Obligatorio para operaciones importantes
- Limitacion de tasa: Proteccion contra fuerza bruta
- Logs de auditoria: Registro de eventos de autenticacion
Con una implementacion adecuada de autenticacion y autorizacion, puede construir aplicaciones seguras.