---
id: "ET-AUTH-002"
title: "JWT Tokens Implementation"
type: "Specification"
status: "Done"
rf_parent: "RF-AUTH-002"
epic: "OQI-001"
version: "1.0"
created_date: "2025-12-05"
updated_date: "2026-01-04"
---

# ET-AUTH-002: Especificación Técnica - JWT Tokens

**Version:** 1.0.0
**Fecha:** 2025-12-05
**Estado:** ✅ Implementado
**Épica:** [OQI-001](../_MAP.md)
**Requerimiento:** [RF-AUTH-002](../requerimientos/RF-AUTH-002-email.md), [RF-AUTH-005](../requerimientos/RF-AUTH-005-sessions.md)

---

## Resumen

Esta especificación detalla la implementación del sistema de tokens JWT para autenticación y autorización en Trading Platform.

---

## Arquitectura de Tokens

```
┌─────────────────────────────────────────────────────────────────────────┐
│                         TOKEN ARCHITECTURE                               │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                         │
│  ┌───────────────────────┐      ┌───────────────────────┐              │
│  │    ACCESS TOKEN       │      │    REFRESH TOKEN      │              │
│  │                       │      │                       │              │
│  │  TTL: 15 minutes      │      │  TTL: 7 days          │              │
│  │  Algorithm: RS256     │      │  Algorithm: RS256     │              │
│  │  Storage: Memory      │      │  Storage: HttpOnly    │              │
│  │                       │      │         Cookie        │              │
│  │  Contains:            │      │                       │              │
│  │  - User ID            │      │  Contains:            │              │
│  │  - Email              │      │  - User ID            │              │
│  │  - Role               │      │  - Token ID (jti)     │              │
│  │  - Permissions        │      │  - Session ID         │              │
│  │                       │      │                       │              │
│  └───────────────────────┘      └───────────────────────┘              │
│           │                              │                              │
│           │ Used for API calls           │ Used to refresh              │
│           │                              │ access token                 │
│           ▼                              ▼                              │
│  ┌───────────────────────┐      ┌───────────────────────┐              │
│  │   API Middleware      │      │   /auth/refresh       │              │
│  │   (authenticate)      │      │   Endpoint            │              │
│  └───────────────────────┘      └───────────────────────┘              │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘
```

---

## Token Service

**Ubicación:** `apps/backend/src/modules/auth/services/token.service.ts`

### Interfaz

```typescript
export interface TokenPayload {
  sub: string;        // User ID
  email: string;
  role: UserRole;
  permissions: string[];
  sessionId?: string;
}

export interface RefreshTokenPayload {
  sub: string;        // User ID
  jti: string;        // Unique token ID
  sessionId: string;
  type: 'refresh';
}

export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  expiresIn: number;  // seconds
  tokenType: 'Bearer';
}

export class TokenService {
  generateTokens(user: User, sessionId?: string): Promise<TokenPair>;
  verifyAccessToken(token: string): Promise<TokenPayload>;
  verifyRefreshToken(token: string): Promise<RefreshTokenPayload>;
  revokeRefreshToken(tokenId: string): Promise<void>;
  rotateRefreshToken(oldToken: string): Promise<TokenPair>;
}
```

### Implementación

```typescript
import jwt from 'jsonwebtoken';
import crypto from 'crypto';

export class TokenService {
  private readonly accessTokenSecret: string;
  private readonly refreshTokenSecret: string;
  private readonly accessTokenExpiry = '15m';
  private readonly refreshTokenExpiry = '7d';

  constructor() {
    this.accessTokenSecret = process.env.JWT_ACCESS_SECRET!;
    this.refreshTokenSecret = process.env.JWT_REFRESH_SECRET!;
  }

  async generateTokens(user: User, sessionId?: string): Promise<TokenPair> {
    const tokenId = crypto.randomUUID();
    const sid = sessionId || crypto.randomUUID();

    const accessToken = jwt.sign(
      {
        sub: user.id,
        email: user.email,
        role: user.role,
        permissions: this.getPermissions(user.role),
      },
      this.accessTokenSecret,
      {
        expiresIn: this.accessTokenExpiry,
        algorithm: 'RS256',
        issuer: 'trading.com',
        audience: 'trading.com',
      }
    );

    const refreshToken = jwt.sign(
      {
        sub: user.id,
        jti: tokenId,
        sessionId: sid,
        type: 'refresh',
      },
      this.refreshTokenSecret,
      {
        expiresIn: this.refreshTokenExpiry,
        algorithm: 'RS256',
        issuer: 'trading.com',
      }
    );

    // Almacenar hash del refresh token en sesión
    await this.storeRefreshToken(user.id, tokenId, sid);

    return {
      accessToken,
      refreshToken,
      expiresIn: 900, // 15 minutes
      tokenType: 'Bearer',
    };
  }

  async verifyAccessToken(token: string): Promise<TokenPayload> {
    try {
      const payload = jwt.verify(token, this.accessTokenSecret, {
        algorithms: ['RS256'],
        issuer: 'trading.com',
        audience: 'trading.com',
      }) as TokenPayload;

      return payload;
    } catch (error) {
      if (error instanceof jwt.TokenExpiredError) {
        throw new UnauthorizedError('Token expired');
      }
      throw new UnauthorizedError('Invalid token');
    }
  }

  async rotateRefreshToken(oldToken: string): Promise<TokenPair> {
    const payload = await this.verifyRefreshToken(oldToken);

    // Verificar que el token no haya sido revocado
    const isValid = await this.validateRefreshToken(payload.jti);
    if (!isValid) {
      // Posible token reuse - revocar todas las sesiones
      await this.revokeAllUserSessions(payload.sub);
      throw new UnauthorizedError('Token has been revoked');
    }

    // Revocar token anterior
    await this.revokeRefreshToken(payload.jti);

    // Obtener usuario
    const user = await this.userService.findById(payload.sub);
    if (!user) {
      throw new UnauthorizedError('User not found');
    }

    // Generar nuevos tokens
    return this.generateTokens(user, payload.sessionId);
  }

  private getPermissions(role: UserRole): string[] {
    const permissions: Record<UserRole, string[]> = {
      investor: ['read:portfolio', 'trade:paper', 'read:education'],
      trader: ['read:portfolio', 'trade:real', 'read:education', 'create:signals'],
      student: ['read:education'],
      admin: ['admin:users', 'admin:content', 'read:*', 'write:*'],
      superadmin: ['*'],
    };
    return permissions[role] || [];
  }
}
```

---

## Estructura de Tokens

### Access Token

```json
// Header
{
  "alg": "RS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "email": "usuario@example.com",
  "role": "investor",
  "permissions": ["read:portfolio", "trade:paper", "read:education"],
  "iat": 1701792000,
  "exp": 1701792900,
  "iss": "trading.com",
  "aud": "trading.com"
}
```

### Refresh Token

```json
// Header
{
  "alg": "RS256",
  "typ": "JWT"
}

// Payload
{
  "sub": "550e8400-e29b-41d4-a716-446655440000",
  "jti": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "sessionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "type": "refresh",
  "iat": 1701792000,
  "exp": 1702396800,
  "iss": "trading.com"
}
```

---

## Middleware de Autenticación

**Ubicación:** `apps/backend/src/core/middleware/auth.middleware.ts`

```typescript
import { Request, Response, NextFunction } from 'express';
import { TokenService } from '@/modules/auth/services/token.service';

export interface AuthenticatedRequest extends Request {
  user: TokenPayload;
}

export const authenticate = async (
  req: Request,
  res: Response,
  next: NextFunction
) => {
  try {
    const authHeader = req.headers.authorization;

    if (!authHeader || !authHeader.startsWith('Bearer ')) {
      throw new UnauthorizedError('Missing or invalid authorization header');
    }

    const token = authHeader.substring(7);
    const tokenService = new TokenService();
    const payload = await tokenService.verifyAccessToken(token);

    (req as AuthenticatedRequest).user = payload;
    next();
  } catch (error) {
    next(error);
  }
};

export const requireRole = (...roles: UserRole[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const authReq = req as AuthenticatedRequest;

    if (!roles.includes(authReq.user.role)) {
      throw new ForbiddenError('Insufficient permissions');
    }

    next();
  };
};

export const requirePermission = (...permissions: string[]) => {
  return (req: Request, res: Response, next: NextFunction) => {
    const authReq = req as AuthenticatedRequest;

    const hasPermission = permissions.every(
      perm => authReq.user.permissions.includes(perm) ||
              authReq.user.permissions.includes('*')
    );

    if (!hasPermission) {
      throw new ForbiddenError('Insufficient permissions');
    }

    next();
  };
};
```

---

## Flujo de Refresh Token

```
┌─────────────┐     ┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Client    │     │   Backend   │     │    Redis    │     │  PostgreSQL │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │                   │
       │ Access token      │                   │                   │
       │ expired           │                   │                   │
       │                   │                   │                   │
       │ POST /auth/refresh│                   │                   │
       │ { refreshToken }  │                   │                   │
       │──────────────────▶│                   │                   │
       │                   │                   │                   │
       │                   │ Verify JWT        │                   │
       │                   │ signature         │                   │
       │                   │                   │                   │
       │                   │ Check if revoked  │                   │
       │                   │──────────────────▶│                   │
       │                   │◀──────────────────│                   │
       │                   │                   │                   │
       │                   │ Get user data     │                   │
       │                   │───────────────────────────────────────▶
       │                   │◀───────────────────────────────────────
       │                   │                   │                   │
       │                   │ Revoke old token  │                   │
       │                   │──────────────────▶│                   │
       │                   │                   │                   │
       │                   │ Generate new pair │                   │
       │                   │                   │                   │
       │                   │ Store new token   │                   │
       │                   │──────────────────▶│                   │
       │                   │                   │                   │
       │◀──────────────────│                   │                   │
       │ { accessToken,    │                   │                   │
       │   refreshToken }  │                   │                   │
```

---

## Almacenamiento de Tokens

### Redis (Token Blacklist & Sessions)

```typescript
// Almacenar refresh token
await redis.setEx(
  `refresh:${userId}:${tokenId}`,
  7 * 24 * 60 * 60, // 7 días
  JSON.stringify({
    sessionId,
    createdAt: Date.now(),
    lastUsed: Date.now(),
  })
);

// Revocar token
await redis.del(`refresh:${userId}:${tokenId}`);

// Revocar todas las sesiones de un usuario
const keys = await redis.keys(`refresh:${userId}:*`);
if (keys.length > 0) {
  await redis.del(keys);
}
```

### Estructura en Redis

```
refresh:{userId}:{tokenId} => {
  "sessionId": "uuid",
  "createdAt": 1701792000000,
  "lastUsed": 1701792000000
}

TTL: 7 días
```

---

## Configuración

### Variables de Entorno

```env
# JWT Secrets (RS256 - usar keys generadas)
JWT_ACCESS_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----...
JWT_ACCESS_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----...
JWT_REFRESH_PRIVATE_KEY=-----BEGIN RSA PRIVATE KEY-----...
JWT_REFRESH_PUBLIC_KEY=-----BEGIN PUBLIC KEY-----...

# Token TTLs
JWT_ACCESS_EXPIRY=15m
JWT_REFRESH_EXPIRY=7d

# Issuer/Audience
JWT_ISSUER=trading.com
JWT_AUDIENCE=trading.com
```

### Generación de Keys RS256

```bash
# Generar key privada
openssl genrsa -out private.key 2048

# Extraer key pública
openssl rsa -in private.key -pubout -out public.key
```

---

## Manejo en Frontend

### Interceptor de Axios

```typescript
// apps/frontend/src/services/api.ts
import axios from 'axios';
import { useAuthStore } from '@/stores/auth.store';

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
});

// Request interceptor - agregar token
api.interceptors.request.use((config) => {
  const { accessToken } = useAuthStore.getState();

  if (accessToken) {
    config.headers.Authorization = `Bearer ${accessToken}`;
  }

  return config;
});

// Response interceptor - refresh on 401
api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;

    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;

      try {
        const { refreshToken, setTokens, logout } = useAuthStore.getState();

        if (!refreshToken) {
          logout();
          return Promise.reject(error);
        }

        const response = await axios.post(
          `${import.meta.env.VITE_API_URL}/auth/refresh`,
          { refreshToken }
        );

        setTokens(response.data);

        originalRequest.headers.Authorization =
          `Bearer ${response.data.accessToken}`;

        return api(originalRequest);
      } catch (refreshError) {
        useAuthStore.getState().logout();
        return Promise.reject(refreshError);
      }
    }

    return Promise.reject(error);
  }
);

export default api;
```

### Auth Store

```typescript
// apps/frontend/src/stores/auth.store.ts
import { create } from 'zustand';
import { persist } from 'zustand/middleware';

interface AuthState {
  user: User | null;
  accessToken: string | null;
  refreshToken: string | null;
  isAuthenticated: boolean;

  setUser: (user: User) => void;
  setTokens: (tokens: TokenPair) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      accessToken: null,
      refreshToken: null,
      isAuthenticated: false,

      setUser: (user) => set({ user, isAuthenticated: true }),

      setTokens: ({ accessToken, refreshToken }) =>
        set({ accessToken, refreshToken }),

      logout: () => set({
        user: null,
        accessToken: null,
        refreshToken: null,
        isAuthenticated: false,
      }),
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        refreshToken: state.refreshToken,
        // No persistir accessToken (memoria)
      }),
    }
  )
);
```

---

## Seguridad

### Protección contra Token Theft

1. **Short-lived access tokens** (15 min)
2. **Refresh token rotation** en cada uso
3. **Token binding** a device/IP (opcional)
4. **Revocación inmediata** posible

### Detección de Token Reuse

```typescript
async rotateRefreshToken(oldToken: string): Promise<TokenPair> {
  const payload = await this.verifyRefreshToken(oldToken);

  // Verificar si el token ya fue usado
  const tokenData = await redis.get(`refresh:${payload.sub}:${payload.jti}`);

  if (!tokenData) {
    // Token ya no existe = fue rotado o revocado
    // Posible robo de token

    // Revocar TODAS las sesiones del usuario
    await this.revokeAllUserSessions(payload.sub);

    // Notificar al usuario
    await this.notifySecurityAlert(payload.sub, 'token_reuse_detected');

    // Log de seguridad
    await this.securityLog.log({
      event: 'TOKEN_REUSE_DETECTED',
      userId: payload.sub,
      tokenId: payload.jti,
      timestamp: new Date(),
    });

    throw new UnauthorizedError('Security alert: Token reuse detected');
  }

  // Proceder con rotación normal...
}
```

---

## Testing

```typescript
describe('TokenService', () => {
  describe('generateTokens', () => {
    it('should generate valid access and refresh tokens', async () => {
      const user = createMockUser();
      const tokens = await tokenService.generateTokens(user);

      expect(tokens.accessToken).toBeDefined();
      expect(tokens.refreshToken).toBeDefined();
      expect(tokens.expiresIn).toBe(900);
      expect(tokens.tokenType).toBe('Bearer');
    });
  });

  describe('verifyAccessToken', () => {
    it('should verify valid token', async () => {
      const user = createMockUser();
      const { accessToken } = await tokenService.generateTokens(user);

      const payload = await tokenService.verifyAccessToken(accessToken);

      expect(payload.sub).toBe(user.id);
      expect(payload.email).toBe(user.email);
      expect(payload.role).toBe(user.role);
    });

    it('should reject expired token', async () => {
      const expiredToken = jwt.sign(
        { sub: 'test' },
        process.env.JWT_ACCESS_SECRET,
        { expiresIn: '-1h' }
      );

      await expect(tokenService.verifyAccessToken(expiredToken))
        .rejects.toThrow('Token expired');
    });
  });

  describe('rotateRefreshToken', () => {
    it('should generate new tokens and revoke old', async () => {
      const user = createMockUser();
      const { refreshToken } = await tokenService.generateTokens(user);

      const newTokens = await tokenService.rotateRefreshToken(refreshToken);

      expect(newTokens.refreshToken).not.toBe(refreshToken);

      // Old token should be revoked
      await expect(tokenService.rotateRefreshToken(refreshToken))
        .rejects.toThrow();
    });
  });
});
```

---

## Referencias

- [JWT RFC 7519](https://tools.ietf.org/html/rfc7519)
- [JWT Best Practices](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-jwt-bcp)
- [OAuth 2.0 Token Revocation](https://tools.ietf.org/html/rfc7009)
