# Especificacion Tecnica Backend - MGN-001 Auth

## Identificacion

| Campo | Valor |
|-------|-------|
| **Modulo** | MGN-001 |
| **Nombre** | Auth - Autenticacion |
| **Version** | 2.0 |
| **Framework** | NestJS |
| **Estado** | Implementado |
| **Autor** | System |
| **Fecha** | 2026-01-10 |

---

## Estructura del Modulo

```
src/modules/auth/
├── auth.module.ts
├── mfa.service.ts                          # MFA/2FA (45 tests)
├── apiKeys.service.ts                      # API Keys
├── controllers/
│   └── auth.controller.ts
├── services/
│   ├── auth.service.ts
│   ├── token.service.ts
│   ├── trusted-devices.service.ts          # Dispositivos confiables (41 tests)
│   ├── email-verification.service.ts       # Verificacion email (32 tests)
│   └── permission-cache.service.ts         # Cache permisos (37 tests)
├── providers/
│   ├── oauth.service.ts                    # OAuth2 Google/Microsoft (32 tests)
│   ├── google.provider.ts
│   └── microsoft.provider.ts
├── guards/
│   ├── jwt-auth.guard.ts
│   └── throttler.guard.ts
├── strategies/
│   └── jwt.strategy.ts
├── decorators/
│   ├── current-user.decorator.ts
│   └── public.decorator.ts
├── dto/
│   ├── login.dto.ts
│   ├── login-response.dto.ts
│   ├── refresh-token.dto.ts
│   ├── token-response.dto.ts
│   ├── request-password-reset.dto.ts
│   ├── reset-password.dto.ts
│   ├── mfa.dto.ts
│   └── email-verification.dto.ts
├── interfaces/
│   ├── jwt-payload.interface.ts
│   └── token-pair.interface.ts
├── entities/
│   ├── refresh-token.entity.ts
│   ├── revoked-token.entity.ts
│   ├── session-history.entity.ts
│   ├── login-attempt.entity.ts
│   ├── password-reset-token.entity.ts
│   ├── password-history.entity.ts
│   ├── user-mfa.entity.ts                  # MFA config por usuario
│   ├── mfa-audit-log.entity.ts             # Auditoria MFA
│   ├── trusted-device.entity.ts            # Dispositivos confiables
│   ├── email-verification-token.entity.ts  # Tokens verificacion email
│   ├── verification-code.entity.ts         # Codigos de verificacion
│   ├── api-key.entity.ts                   # API Keys
│   ├── oauth-provider.entity.ts            # Configuracion OAuth
│   ├── oauth-state.entity.ts               # Estados OAuth CSRF
│   └── oauth-user-link.entity.ts           # Vinculos OAuth-Usuario
└── constants/
    └── auth.constants.ts
```

---

## Entidades

### RefreshToken

```typescript
// entities/refresh-token.entity.ts
import {
  Entity, PrimaryGeneratedColumn, Column, ManyToOne,
  CreateDateColumn, Index
} from 'typeorm';
import { User } from '../../users/entities/user.entity';
import { Tenant } from '../../tenants/entities/tenant.entity';

@Entity({ schema: 'core_auth', name: 'refresh_tokens' })
@Index(['userId', 'tenantId'], { where: '"revoked_at" IS NULL AND "is_used" = false' })
export class RefreshToken {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'user_id', type: 'uuid' })
  userId: string;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  user: User;

  @Column({ name: 'tenant_id', type: 'uuid' })
  tenantId: string;

  @ManyToOne(() => Tenant, { onDelete: 'CASCADE' })
  tenant: Tenant;

  @Column({ type: 'varchar', length: 64, unique: true })
  jti: string;

  @Column({ name: 'token_hash', type: 'varchar', length: 255 })
  tokenHash: string;

  @Column({ name: 'family_id', type: 'uuid' })
  familyId: string;

  @Column({ name: 'is_used', type: 'boolean', default: false })
  isUsed: boolean;

  @Column({ name: 'used_at', type: 'timestamptz', nullable: true })
  usedAt: Date | null;

  @Column({ name: 'replaced_by', type: 'uuid', nullable: true })
  replacedBy: string | null;

  @Column({ name: 'device_info', type: 'varchar', length: 500, nullable: true })
  deviceInfo: string | null;

  @Column({ name: 'ip_address', type: 'inet', nullable: true })
  ipAddress: string | null;

  @Column({ name: 'expires_at', type: 'timestamptz' })
  expiresAt: Date;

  @Column({ name: 'revoked_at', type: 'timestamptz', nullable: true })
  revokedAt: Date | null;

  @Column({ name: 'revoked_reason', type: 'varchar', length: 50, nullable: true })
  revokedReason: string | null;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;
}
```

### SessionHistory

```typescript
// entities/session-history.entity.ts
@Entity({ schema: 'core_auth', name: 'session_history' })
export class SessionHistory {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'user_id', type: 'uuid' })
  userId: string;

  @Column({ name: 'tenant_id', type: 'uuid' })
  tenantId: string;

  @Column({ type: 'varchar', length: 30 })
  action: SessionAction;

  @Column({ name: 'device_info', type: 'varchar', length: 500, nullable: true })
  deviceInfo: string | null;

  @Column({ name: 'ip_address', type: 'inet', nullable: true })
  ipAddress: string | null;

  @Column({ type: 'jsonb', default: {} })
  metadata: Record<string, any>;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;
}

export type SessionAction =
  | 'login'
  | 'logout'
  | 'logout_all'
  | 'refresh'
  | 'password_change'
  | 'password_reset'
  | 'account_locked'
  | 'account_unlocked';
```

### LoginAttempt

```typescript
// entities/login-attempt.entity.ts
@Entity({ schema: 'core_auth', name: 'login_attempts' })
export class LoginAttempt {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'varchar', length: 255 })
  email: string;

  @Column({ name: 'tenant_id', type: 'uuid', nullable: true })
  tenantId: string | null;

  @Column({ name: 'user_id', type: 'uuid', nullable: true })
  userId: string | null;

  @Column({ type: 'boolean' })
  success: boolean;

  @Column({ name: 'failure_reason', type: 'varchar', length: 50, nullable: true })
  failureReason: string | null;

  @Column({ name: 'ip_address', type: 'inet' })
  ipAddress: string;

  @Column({ name: 'user_agent', type: 'varchar', length: 500, nullable: true })
  userAgent: string | null;

  @Column({ name: 'attempted_at', type: 'timestamptz', default: () => 'NOW()' })
  attemptedAt: Date;
}
```

### PasswordResetToken

```typescript
// entities/password-reset-token.entity.ts
@Entity({ schema: 'core_auth', name: 'password_reset_tokens' })
export class PasswordResetToken {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ name: 'user_id', type: 'uuid' })
  userId: string;

  @Column({ name: 'tenant_id', type: 'uuid' })
  tenantId: string;

  @Column({ name: 'token_hash', type: 'varchar', length: 255 })
  tokenHash: string;

  @Column({ type: 'integer', default: 0 })
  attempts: number;

  @Column({ name: 'expires_at', type: 'timestamptz' })
  expiresAt: Date;

  @Column({ name: 'used_at', type: 'timestamptz', nullable: true })
  usedAt: Date | null;

  @Column({ name: 'invalidated_at', type: 'timestamptz', nullable: true })
  invalidatedAt: Date | null;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;
}
```

### UserMfa

```typescript
// entities/user-mfa.entity.ts
export enum MfaMethod {
  NONE = 'none',
  TOTP = 'totp',
  SMS = 'sms',
  EMAIL = 'email',
}

export enum MfaStatus {
  DISABLED = 'disabled',
  PENDING_SETUP = 'pending_setup',
  ENABLED = 'enabled',
}

@Entity({ schema: 'auth', name: 'user_mfa' })
@Index('idx_user_mfa_user', ['userId'], { unique: true })
@Index('idx_user_mfa_status', ['status'], { where: "status = 'enabled'" })
export class UserMfa {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid', nullable: false, name: 'user_id', unique: true })
  userId: string;

  @Column({ type: 'enum', enum: MfaMethod, default: MfaMethod.NONE })
  method: MfaMethod;

  @Column({ type: 'enum', enum: MfaStatus, default: MfaStatus.DISABLED })
  status: MfaStatus;

  @Column({ type: 'varchar', length: 256, nullable: true, name: 'totp_secret' })
  totpSecret: string | null;

  @Column({ type: 'jsonb', default: [], name: 'backup_codes_hashes' })
  backupCodesHashes: string[];

  @Column({ type: 'integer', default: 0, name: 'backup_codes_used' })
  backupCodesUsed: number;

  @Column({ type: 'integer', default: 0, name: 'backup_codes_total' })
  backupCodesTotal: number;

  @Column({ type: 'timestamptz', nullable: true, name: 'backup_codes_regenerated_at' })
  backupCodesRegeneratedAt: Date | null;

  @Column({ type: 'timestamptz', nullable: true, name: 'enabled_at' })
  enabledAt: Date | null;

  @Column({ type: 'timestamptz', nullable: true, name: 'last_verified_at' })
  lastVerifiedAt: Date | null;

  @Column({ type: 'integer', default: 0, name: 'failed_attempts' })
  failedAttempts: number;

  @Column({ type: 'timestamptz', nullable: true, name: 'locked_until' })
  lockedUntil: Date | null;

  @OneToOne(() => User, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  user: User;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz', nullable: true })
  updatedAt: Date | null;
}
```

### TrustedDevice

```typescript
// entities/trusted-device.entity.ts
export enum TrustLevel {
  STANDARD = 'standard',   // 30 dias
  HIGH = 'high',           // 90 dias
  TEMPORARY = 'temporary', // 24 horas
}

@Entity({ schema: 'auth', name: 'trusted_devices' })
@Index('idx_trusted_devices_user', ['userId'], { where: 'is_active' })
@Index('idx_trusted_devices_fingerprint', ['deviceFingerprint'])
export class TrustedDevice {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid', nullable: false, name: 'user_id' })
  userId: string;

  @Column({ type: 'varchar', length: 128, nullable: false, name: 'device_fingerprint' })
  deviceFingerprint: string;

  @Column({ type: 'varchar', length: 128, nullable: true, name: 'device_name' })
  deviceName: string | null;

  @Column({ type: 'varchar', length: 32, nullable: true, name: 'device_type' })
  deviceType: string | null;

  @Column({ type: 'text', nullable: true, name: 'user_agent' })
  userAgent: string | null;

  @Column({ type: 'varchar', length: 64, nullable: true, name: 'browser_name' })
  browserName: string | null;

  @Column({ type: 'varchar', length: 64, nullable: true, name: 'os_name' })
  osName: string | null;

  @Column({ type: 'inet', nullable: false, name: 'registered_ip' })
  registeredIp: string;

  @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' })
  isActive: boolean;

  @Column({ type: 'enum', enum: TrustLevel, default: TrustLevel.STANDARD, name: 'trust_level' })
  trustLevel: TrustLevel;

  @Column({ type: 'timestamptz', nullable: true, name: 'trust_expires_at' })
  trustExpiresAt: Date | null;

  @Column({ type: 'timestamptz', nullable: false, name: 'last_used_at' })
  lastUsedAt: Date;

  @Column({ type: 'inet', nullable: true, name: 'last_used_ip' })
  lastUsedIp: string | null;

  @Column({ type: 'integer', default: 1, nullable: false, name: 'use_count' })
  useCount: number;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  user: User;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;

  @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' })
  revokedAt: Date | null;

  @Column({ type: 'varchar', length: 128, nullable: true, name: 'revoked_reason' })
  revokedReason: string | null;
}
```

### EmailVerificationToken

```typescript
// entities/email-verification-token.entity.ts
@Entity({ schema: 'auth', name: 'email_verification_tokens' })
@Index('idx_email_verification_tokens_user_id', ['userId'])
@Index('idx_email_verification_tokens_token', ['token'])
@Index('idx_email_verification_tokens_expires_at', ['expiresAt'])
export class EmailVerificationToken {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid', nullable: false, name: 'user_id' })
  userId: string;

  @Column({ type: 'varchar', length: 500, unique: true, nullable: false })
  token: string;

  @Column({ type: 'varchar', length: 255, nullable: false })
  email: string;

  @Column({ type: 'timestamp', nullable: false, name: 'expires_at' })
  expiresAt: Date;

  @Column({ type: 'timestamp', nullable: true, name: 'used_at' })
  usedAt: Date | null;

  @Column({ type: 'inet', nullable: true, name: 'ip_address' })
  ipAddress: string | null;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  user: User;

  @CreateDateColumn({ name: 'created_at', type: 'timestamp' })
  createdAt: Date;

  // Metodos de utilidad
  isExpired(): boolean { return new Date() > this.expiresAt; }
  isUsed(): boolean { return this.usedAt !== null; }
  isValid(): boolean { return !this.isExpired() && !this.isUsed(); }
}
```

### ApiKey

```typescript
// entities/api-key.entity.ts
@Entity({ schema: 'auth', name: 'api_keys' })
@Index('idx_api_keys_lookup', ['keyIndex', 'isActive'], { where: 'is_active = TRUE' })
@Index('idx_api_keys_expiration', ['expirationDate'], { where: 'expiration_date IS NOT NULL' })
@Index('idx_api_keys_user', ['userId'])
@Index('idx_api_keys_tenant', ['tenantId'])
export class ApiKey {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid', nullable: false, name: 'user_id' })
  userId: string;

  @Column({ type: 'uuid', nullable: false, name: 'tenant_id' })
  tenantId: string;

  @Column({ type: 'varchar', length: 255, nullable: false })
  name: string;

  @Column({ type: 'varchar', length: 16, nullable: false, name: 'key_index' })
  keyIndex: string;

  @Column({ type: 'varchar', length: 255, nullable: false, name: 'key_hash' })
  keyHash: string;

  @Column({ type: 'varchar', length: 100, nullable: true })
  scope: string | null;

  @Column({ type: 'inet', array: true, nullable: true, name: 'allowed_ips' })
  allowedIps: string[] | null;

  @Column({ type: 'timestamptz', nullable: true, name: 'expiration_date' })
  expirationDate: Date | null;

  @Column({ type: 'timestamptz', nullable: true, name: 'last_used_at' })
  lastUsedAt: Date | null;

  @Column({ type: 'boolean', default: true, nullable: false, name: 'is_active' })
  isActive: boolean;

  @ManyToOne(() => User, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'user_id' })
  user: User;

  @ManyToOne(() => Tenant, { onDelete: 'CASCADE' })
  @JoinColumn({ name: 'tenant_id' })
  tenant: Tenant;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;

  @Column({ type: 'timestamptz', nullable: true, name: 'revoked_at' })
  revokedAt: Date | null;

  @Column({ type: 'uuid', nullable: true, name: 'revoked_by' })
  revokedBy: string | null;
}
```

### OAuthProvider

```typescript
// entities/oauth-provider.entity.ts
@Entity({ schema: 'auth', name: 'oauth_providers' })
@Index('idx_oauth_providers_enabled', ['isEnabled'])
@Index('idx_oauth_providers_tenant', ['tenantId'])
@Index('idx_oauth_providers_code', ['code'])
export class OAuthProvider {
  @PrimaryGeneratedColumn('uuid')
  id: string;

  @Column({ type: 'uuid', nullable: true, name: 'tenant_id' })
  tenantId: string | null;

  @Column({ type: 'varchar', length: 50, nullable: false, unique: true })
  code: string;

  @Column({ type: 'varchar', length: 100, nullable: false })
  name: string;

  // Configuracion OAuth2
  @Column({ type: 'varchar', length: 255, nullable: false, name: 'client_id' })
  clientId: string;

  @Column({ type: 'varchar', length: 500, nullable: true, name: 'client_secret' })
  clientSecret: string | null;

  // Endpoints OAuth2
  @Column({ type: 'varchar', length: 500, nullable: false, name: 'authorization_endpoint' })
  authorizationEndpoint: string;

  @Column({ type: 'varchar', length: 500, nullable: false, name: 'token_endpoint' })
  tokenEndpoint: string;

  @Column({ type: 'varchar', length: 500, nullable: false, name: 'userinfo_endpoint' })
  userinfoEndpoint: string;

  @Column({ type: 'varchar', length: 500, default: 'openid profile email' })
  scope: string;

  // PKCE
  @Column({ type: 'boolean', default: true, name: 'pkce_enabled' })
  pkceEnabled: boolean;

  @Column({ type: 'varchar', length: 10, default: 'S256', name: 'code_challenge_method' })
  codeChallengeMethod: string | null;

  // Mapeo de claims
  @Column({ type: 'jsonb', name: 'claim_mapping', default: { sub: 'oauth_uid', email: 'email', name: 'name', picture: 'avatar_url' } })
  claimMapping: Record<string, any>;

  // UI
  @Column({ type: 'varchar', length: 100, nullable: true, name: 'button_text' })
  buttonText: string | null;

  @Column({ type: 'boolean', default: false, name: 'is_enabled' })
  isEnabled: boolean;

  @Column({ type: 'text', array: true, nullable: true, name: 'allowed_domains' })
  allowedDomains: string[] | null;

  @Column({ type: 'boolean', default: false, name: 'auto_create_users' })
  autoCreateUsers: boolean;

  @ManyToOne(() => Tenant, { onDelete: 'CASCADE', nullable: true })
  @JoinColumn({ name: 'tenant_id' })
  tenant: Tenant | null;

  @CreateDateColumn({ name: 'created_at', type: 'timestamptz' })
  createdAt: Date;

  @UpdateDateColumn({ name: 'updated_at', type: 'timestamptz' })
  updatedAt: Date;
}
```

---

## Interfaces

### JwtPayload

```typescript
// interfaces/jwt-payload.interface.ts
export interface JwtPayload {
  sub: string;      // User ID
  tid: string;      // Tenant ID
  email: string;
  roles: string[];
  permissions?: string[];
  iat: number;      // Issued At
  exp: number;      // Expiration
  iss: string;      // Issuer
  aud: string;      // Audience
  jti: string;      // JWT ID
}

export interface JwtRefreshPayload extends Pick<JwtPayload, 'sub' | 'tid' | 'jti' | 'iat' | 'exp' | 'iss' | 'aud'> {
  type: 'refresh';
}
```

### TokenPair

```typescript
// interfaces/token-pair.interface.ts
export interface TokenPair {
  accessToken: string;
  refreshToken: string;
  accessTokenExpiresAt: Date;
  refreshTokenExpiresAt: Date;
  refreshTokenId: string;
}
```

---

## DTOs

### LoginDto

```typescript
// dto/login.dto.ts
import { IsEmail, IsString, MinLength, MaxLength, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class LoginDto {
  @ApiProperty({
    description: 'Email del usuario',
    example: 'user@example.com',
  })
  @IsEmail({}, { message: 'Email inválido' })
  @IsNotEmpty({ message: 'Email es requerido' })
  email: string;

  @ApiProperty({
    description: 'Contraseña del usuario',
    example: 'SecurePass123!',
    minLength: 8,
  })
  @IsString()
  @MinLength(8, { message: 'Password debe tener mínimo 8 caracteres' })
  @MaxLength(128, { message: 'Password no puede exceder 128 caracteres' })
  @IsNotEmpty({ message: 'Password es requerido' })
  password: string;
}
```

### LoginResponseDto

```typescript
// dto/login-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';

export class LoginResponseDto {
  @ApiProperty({
    description: 'Access token JWT',
    example: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...',
  })
  accessToken: string;

  @ApiProperty({
    description: 'Refresh token JWT',
    example: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...',
  })
  refreshToken: string;

  @ApiProperty({
    description: 'Tipo de token',
    example: 'Bearer',
  })
  tokenType: string;

  @ApiProperty({
    description: 'Tiempo de expiración en segundos',
    example: 900,
  })
  expiresIn: number;

  @ApiProperty({
    description: 'Información básica del usuario',
  })
  user: {
    id: string;
    email: string;
    firstName: string;
    lastName: string;
    roles: string[];
  };
}
```

### RefreshTokenDto

```typescript
// dto/refresh-token.dto.ts
import { IsString, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class RefreshTokenDto {
  @ApiProperty({
    description: 'Refresh token para renovar sesión',
    example: 'eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...',
  })
  @IsString()
  @IsNotEmpty({ message: 'Refresh token es requerido' })
  refreshToken: string;
}
```

### TokenResponseDto

```typescript
// dto/token-response.dto.ts
import { ApiProperty } from '@nestjs/swagger';

export class TokenResponseDto {
  @ApiProperty()
  accessToken: string;

  @ApiProperty()
  refreshToken: string;

  @ApiProperty()
  tokenType: string;

  @ApiProperty()
  expiresIn: number;
}
```

### RequestPasswordResetDto

```typescript
// dto/request-password-reset.dto.ts
import { IsEmail, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class RequestPasswordResetDto {
  @ApiProperty({
    description: 'Email del usuario',
    example: 'user@example.com',
  })
  @IsEmail({}, { message: 'Email inválido' })
  @IsNotEmpty({ message: 'Email es requerido' })
  email: string;
}
```

### ResetPasswordDto

```typescript
// dto/reset-password.dto.ts
import { IsString, MinLength, MaxLength, Matches, IsNotEmpty } from 'class-validator';
import { ApiProperty } from '@nestjs/swagger';

export class ResetPasswordDto {
  @ApiProperty({
    description: 'Token de recuperación',
  })
  @IsString()
  @IsNotEmpty({ message: 'Token es requerido' })
  token: string;

  @ApiProperty({
    description: 'Nueva contraseña',
    minLength: 8,
  })
  @IsString()
  @MinLength(8, { message: 'Password debe tener mínimo 8 caracteres' })
  @MaxLength(128, { message: 'Password no puede exceder 128 caracteres' })
  @Matches(
    /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]/,
    {
      message: 'Password debe incluir mayúscula, minúscula, número y carácter especial',
    },
  )
  @IsNotEmpty({ message: 'Password es requerido' })
  newPassword: string;

  @ApiProperty({
    description: 'Confirmación de nueva contraseña',
  })
  @IsString()
  @IsNotEmpty({ message: 'Confirmación de password es requerida' })
  confirmPassword: string;
}
```

---

## Endpoints

### Resumen de Endpoints

| Metodo | Ruta | Descripcion | Auth | Rate Limit |
|--------|------|-------------|------|------------|
| POST | `/api/v1/auth/login` | Autenticar usuario | No | 10/min/IP |
| POST | `/api/v1/auth/refresh` | Renovar tokens | No | 1/seg |
| POST | `/api/v1/auth/logout` | Cerrar sesion | Si | 10/min |
| POST | `/api/v1/auth/logout-all` | Cerrar todas las sesiones | Si | 5/min |
| POST | `/api/v1/auth/password/request-reset` | Solicitar recuperacion | No | 3/hora/email |
| POST | `/api/v1/auth/password/reset` | Cambiar password | No | 5/hora/IP |
| GET | `/api/v1/auth/password/validate-token/:token` | Validar token | No | 10/min |

---

### POST /api/v1/auth/login

Autentica un usuario con email y password.

#### Request

```typescript
// Headers
{
  "Content-Type": "application/json",
  "X-Tenant-Id": "tenant-uuid"  // Opcional si tenant único
}

// Body
{
  "email": "user@example.com",
  "password": "SecurePass123!"
}
```

#### Response Success (200)

```typescript
{
  "accessToken": "eyJhbGciOiJSUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJSUzI1NiIs...",
  "tokenType": "Bearer",
  "expiresIn": 900,
  "user": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "email": "user@example.com",
    "firstName": "John",
    "lastName": "Doe",
    "roles": ["admin"]
  }
}
```

#### Response Errors

| Code | Mensaje | Descripcion |
|------|---------|-------------|
| 400 | "Datos de entrada inválidos" | Validación fallida |
| 401 | "Credenciales inválidas" | Email/password incorrecto |
| 423 | "Cuenta bloqueada" | Demasiados intentos |
| 403 | "Cuenta inactiva" | Usuario deshabilitado |

---

### POST /api/v1/auth/refresh

Renueva el par de tokens usando un refresh token válido.

#### Request

```typescript
// Headers
{
  "Content-Type": "application/json"
}

// Body
{
  "refreshToken": "eyJhbGciOiJSUzI1NiIs..."
}

// O mediante httpOnly cookie (preferido)
// Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs...
```

#### Response Success (200)

```typescript
{
  "accessToken": "eyJhbGciOiJSUzI1NiIs...",
  "refreshToken": "eyJhbGciOiJSUzI1NiIs...",
  "tokenType": "Bearer",
  "expiresIn": 900
}
```

#### Response Errors

| Code | Mensaje | Descripcion |
|------|---------|-------------|
| 400 | "Refresh token requerido" | No se envió token |
| 401 | "Refresh token expirado" | Token expiró |
| 401 | "Sesión comprometida" | Token replay detectado |
| 401 | "Token revocado" | Token fue revocado |

---

### POST /api/v1/auth/logout

Cierra la sesión actual del usuario.

#### Request

```typescript
// Headers
{
  "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
}

// Cookie (refresh token)
// Cookie: refresh_token=eyJhbGciOiJSUzI1NiIs...
```

#### Response Success (200)

```typescript
{
  "message": "Sesión cerrada exitosamente"
}

// Set-Cookie: refresh_token=; Max-Age=0; HttpOnly; Secure; SameSite=Strict
```

---

### POST /api/v1/auth/logout-all

Cierra todas las sesiones activas del usuario.

#### Request

```typescript
// Headers
{
  "Authorization": "Bearer eyJhbGciOiJSUzI1NiIs..."
}
```

#### Response Success (200)

```typescript
{
  "message": "Todas las sesiones han sido cerradas",
  "sessionsRevoked": 3
}
```

---

### POST /api/v1/auth/password/request-reset

Solicita un enlace de recuperación de contraseña.

#### Request

```typescript
{
  "email": "user@example.com"
}
```

#### Response Success (200)

```typescript
{
  "message": "Si el email está registrado, recibirás instrucciones para restablecer tu contraseña"
}
```

> **Nota:** Siempre retorna 200 con mensaje genérico para no revelar existencia de emails.

---

### POST /api/v1/auth/password/reset

Establece una nueva contraseña usando el token de recuperación.

#### Request

```typescript
{
  "token": "a1b2c3d4e5f6...",
  "newPassword": "NewSecurePass123!",
  "confirmPassword": "NewSecurePass123!"
}
```

#### Response Success (200)

```typescript
{
  "message": "Contraseña actualizada exitosamente. Por favor inicia sesión."
}
```

#### Response Errors

| Code | Mensaje | Descripcion |
|------|---------|-------------|
| 400 | "Token de recuperación expirado" | Token > 1 hora |
| 400 | "Token ya fue utilizado" | Token usado |
| 400 | "Token invalidado" | 3+ intentos fallidos |
| 400 | "Las contraseñas no coinciden" | Confirmación diferente |
| 400 | "No puede ser igual a contraseñas anteriores" | Historial |

---

### GET /api/v1/auth/password/validate-token/:token

Valida si un token de recuperación es válido antes de mostrar el formulario.

#### Request

```
GET /api/v1/auth/password/validate-token/a1b2c3d4e5f6...
```

#### Response Success (200)

```typescript
{
  "valid": true,
  "email": "u***@example.com"  // Email parcialmente oculto
}
```

#### Response Invalid (400)

```typescript
{
  "valid": false,
  "reason": "expired" | "used" | "invalid"
}
```

---

## Services

### AuthService

```typescript
// services/auth.service.ts
import { Injectable, UnauthorizedException, ForbiddenException } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import * as bcrypt from 'bcrypt';
import { User } from '../../users/entities/user.entity';
import { TokenService } from './token.service';
import { LoginDto } from '../dto/login.dto';
import { LoginResponseDto } from '../dto/login-response.dto';
import { LoginAttempt } from '../entities/login-attempt.entity';
import { SessionHistory } from '../entities/session-history.entity';

@Injectable()
export class AuthService {
  private readonly MAX_FAILED_ATTEMPTS = 5;
  private readonly LOCK_DURATION_MINUTES = 30;

  constructor(
    @InjectRepository(User)
    private readonly userRepository: Repository<User>,
    @InjectRepository(LoginAttempt)
    private readonly loginAttemptRepository: Repository<LoginAttempt>,
    @InjectRepository(SessionHistory)
    private readonly sessionHistoryRepository: Repository<SessionHistory>,
    private readonly tokenService: TokenService,
  ) {}

  async login(dto: LoginDto, metadata: RequestMetadata): Promise<LoginResponseDto> {
    // 1. Buscar usuario por email
    const user = await this.userRepository.findOne({
      where: { email: dto.email.toLowerCase() },
      relations: ['roles'],
    });

    // 2. Verificar si existe
    if (!user) {
      await this.recordLoginAttempt(dto.email, null, false, 'invalid_credentials', metadata);
      throw new UnauthorizedException('Credenciales inválidas');
    }

    // 3. Verificar si está bloqueado
    if (this.isAccountLocked(user)) {
      await this.recordLoginAttempt(dto.email, user.id, false, 'account_locked', metadata);
      throw new ForbiddenException('Cuenta bloqueada. Intenta de nuevo más tarde.');
    }

    // 4. Verificar si está activo
    if (!user.isActive) {
      await this.recordLoginAttempt(dto.email, user.id, false, 'account_inactive', metadata);
      throw new ForbiddenException('Cuenta inactiva');
    }

    // 5. Verificar password
    const isPasswordValid = await bcrypt.compare(dto.password, user.passwordHash);
    if (!isPasswordValid) {
      await this.handleFailedLogin(user, metadata);
      throw new UnauthorizedException('Credenciales inválidas');
    }

    // 6. Login exitoso - resetear contador
    await this.resetFailedAttempts(user);

    // 7. Generar tokens
    const tokens = await this.tokenService.generateTokenPair(user, metadata);

    // 8. Registrar sesión
    await this.recordLoginAttempt(dto.email, user.id, true, null, metadata);
    await this.recordSessionHistory(user.id, user.tenantId, 'login', metadata);

    // 9. Construir respuesta
    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
      tokenType: 'Bearer',
      expiresIn: 900, // 15 minutos
      user: {
        id: user.id,
        email: user.email,
        firstName: user.firstName,
        lastName: user.lastName,
        roles: user.roles.map(r => r.name),
      },
    };
  }

  async logout(userId: string, tenantId: string, refreshTokenJti: string, accessTokenJti: string): Promise<void> {
    // 1. Revocar refresh token
    await this.tokenService.revokeRefreshToken(refreshTokenJti, 'user_logout');

    // 2. Blacklistear access token
    await this.tokenService.blacklistAccessToken(accessTokenJti);

    // 3. Registrar en historial
    await this.recordSessionHistory(userId, tenantId, 'logout', {});
  }

  async logoutAll(userId: string, tenantId: string): Promise<number> {
    // 1. Revocar todos los refresh tokens
    const revokedCount = await this.tokenService.revokeAllUserTokens(userId, 'logout_all');

    // 2. Registrar en historial
    await this.recordSessionHistory(userId, tenantId, 'logout_all', {
      sessionsRevoked: revokedCount,
    });

    return revokedCount;
  }

  private isAccountLocked(user: User): boolean {
    if (!user.lockedUntil) return false;
    return new Date() < user.lockedUntil;
  }

  private async handleFailedLogin(user: User, metadata: RequestMetadata): Promise<void> {
    user.failedLoginAttempts = (user.failedLoginAttempts || 0) + 1;

    if (user.failedLoginAttempts >= this.MAX_FAILED_ATTEMPTS) {
      user.lockedUntil = new Date(Date.now() + this.LOCK_DURATION_MINUTES * 60 * 1000);
      await this.recordSessionHistory(user.id, user.tenantId, 'account_locked', {});
    }

    await this.userRepository.save(user);
    await this.recordLoginAttempt(user.email, user.id, false, 'invalid_credentials', metadata);
  }

  private async resetFailedAttempts(user: User): Promise<void> {
    if (user.failedLoginAttempts > 0 || user.lockedUntil) {
      user.failedLoginAttempts = 0;
      user.lockedUntil = null;
      await this.userRepository.save(user);
    }
  }

  private async recordLoginAttempt(
    email: string,
    userId: string | null,
    success: boolean,
    failureReason: string | null,
    metadata: RequestMetadata,
  ): Promise<void> {
    await this.loginAttemptRepository.save({
      email,
      userId,
      success,
      failureReason,
      ipAddress: metadata.ipAddress,
      userAgent: metadata.userAgent,
    });
  }

  private async recordSessionHistory(
    userId: string,
    tenantId: string,
    action: SessionAction,
    metadata: Record<string, any>,
  ): Promise<void> {
    await this.sessionHistoryRepository.save({
      userId,
      tenantId,
      action,
      metadata,
    });
  }
}

interface RequestMetadata {
  ipAddress: string;
  userAgent: string;
  deviceInfo?: string;
}
```

### TokenService

```typescript
// services/token.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository, IsNull } from 'typeorm';
import * as crypto from 'crypto';
import * as bcrypt from 'bcrypt';
import { v4 as uuidv4 } from 'uuid';
import { RefreshToken } from '../entities/refresh-token.entity';
import { JwtPayload, JwtRefreshPayload } from '../interfaces/jwt-payload.interface';
import { TokenPair } from '../interfaces/token-pair.interface';
import { PermissionCacheService } from './permission-cache.service';

@Injectable()
export class TokenService {
  private readonly accessTokenExpiry = '15m';
  private readonly refreshTokenExpiry = '7d';

  constructor(
    private readonly jwtService: JwtService,
    private readonly configService: ConfigService,
    @InjectRepository(RefreshToken)
    private readonly refreshTokenRepository: Repository<RefreshToken>,
    private readonly permissionCacheService: PermissionCacheService,
  ) {}

  async generateTokenPair(user: any, metadata: any): Promise<TokenPair> {
    const accessTokenJti = uuidv4();
    const refreshTokenJti = uuidv4();
    const familyId = uuidv4();

    // Generar Access Token
    const accessPayload: JwtPayload = {
      sub: user.id,
      tid: user.tenantId,
      email: user.email,
      roles: user.roles.map((r: any) => r.name),
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 900, // 15 min
      iss: 'erp-core',
      aud: 'erp-api',
      jti: accessTokenJti,
    };

    const accessToken = this.jwtService.sign(accessPayload, {
      algorithm: 'RS256',
      privateKey: this.configService.get('JWT_PRIVATE_KEY'),
    });

    // Generar Refresh Token
    const refreshPayload: JwtRefreshPayload = {
      sub: user.id,
      tid: user.tenantId,
      jti: refreshTokenJti,
      type: 'refresh',
      iat: Math.floor(Date.now() / 1000),
      exp: Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60, // 7 días
      iss: 'erp-core',
      aud: 'erp-api',
    };

    const refreshToken = this.jwtService.sign(refreshPayload, {
      algorithm: 'RS256',
      privateKey: this.configService.get('JWT_PRIVATE_KEY'),
    });

    // Almacenar refresh token en BD
    const tokenHash = await bcrypt.hash(refreshToken, 10);
    const expiresAt = new Date(Date.now() + 7 * 24 * 60 * 60 * 1000);

    await this.refreshTokenRepository.save({
      userId: user.id,
      tenantId: user.tenantId,
      jti: refreshTokenJti,
      tokenHash,
      familyId,
      ipAddress: metadata.ipAddress,
      deviceInfo: metadata.userAgent,
      expiresAt,
    });

    return {
      accessToken,
      refreshToken,
      accessTokenExpiresAt: new Date(accessPayload.exp * 1000),
      refreshTokenExpiresAt: expiresAt,
      refreshTokenId: refreshTokenJti,
    };
  }

  async refreshTokens(refreshToken: string): Promise<TokenPair> {
    // 1. Decodificar token
    let decoded: JwtRefreshPayload;
    try {
      decoded = this.jwtService.verify(refreshToken, {
        algorithms: ['RS256'],
        publicKey: this.configService.get('JWT_PUBLIC_KEY'),
      });
    } catch (error) {
      throw new UnauthorizedException('Refresh token inválido');
    }

    // 2. Buscar en BD
    const storedToken = await this.refreshTokenRepository.findOne({
      where: { jti: decoded.jti },
    });

    if (!storedToken) {
      throw new UnauthorizedException('Refresh token no encontrado');
    }

    // 3. Verificar si está revocado
    if (storedToken.revokedAt) {
      throw new UnauthorizedException('Token revocado');
    }

    // 4. Detectar reuso (token replay attack)
    if (storedToken.isUsed) {
      // ALERTA DE SEGURIDAD: Token replay detectado
      await this.revokeTokenFamily(storedToken.familyId);
      throw new UnauthorizedException('Sesión comprometida. Por favor inicia sesión nuevamente.');
    }

    // 5. Verificar expiración
    if (new Date() > storedToken.expiresAt) {
      throw new UnauthorizedException('Refresh token expirado');
    }

    // 6. Marcar como usado
    storedToken.isUsed = true;
    storedToken.usedAt = new Date();

    // 7. Obtener usuario
    const user = await this.getUserForRefresh(decoded.sub);

    // 8. Generar nuevos tokens (misma familia)
    const newTokens = await this.generateTokenPairWithFamily(
      user,
      storedToken.familyId,
      { ipAddress: storedToken.ipAddress, userAgent: storedToken.deviceInfo },
    );

    // 9. Actualizar token anterior con referencia al nuevo
    storedToken.replacedBy = newTokens.refreshTokenId;
    await this.refreshTokenRepository.save(storedToken);

    return newTokens;
  }

  async revokeRefreshToken(jti: string, reason: string): Promise<void> {
    await this.refreshTokenRepository.update(
      { jti },
      { revokedAt: new Date(), revokedReason: reason },
    );
  }

  async revokeAllUserTokens(userId: string, reason: string): Promise<number> {
    const result = await this.refreshTokenRepository.update(
      { userId, revokedAt: IsNull() },
      { revokedAt: new Date(), revokedReason: reason },
    );
    return result.affected || 0;
  }

  async revokeTokenFamily(familyId: string): Promise<void> {
    await this.refreshTokenRepository.update(
      { familyId, revokedAt: IsNull() },
      { revokedAt: new Date(), revokedReason: 'security_breach' },
    );
  }

  /**
   * Invalida el cache de permisos para un usuario
   * Se llama cuando se revocan tokens o cambian permisos
   */
  async invalidatePermissionCache(userId: string): Promise<void> {
    await this.permissionCacheService.invalidateAllForUser(userId);
  }

  private async generateTokenPairWithFamily(
    user: any,
    familyId: string,
    metadata: any,
  ): Promise<TokenPair> {
    // Similar a generateTokenPair pero usa familyId existente
    // ... implementation
    return {} as TokenPair; // Placeholder
  }

  private async getUserForRefresh(userId: string): Promise<any> {
    // Obtener usuario con roles actualizados
    // ... implementation
    return {}; // Placeholder
  }
}
```

### MfaService

```typescript
// mfa.service.ts
/**
 * Servicio de Autenticacion Multi-Factor (MFA/2FA)
 *
 * Funcionalidades:
 * - Configuracion TOTP con QR code
 * - Verificacion de codigos TOTP
 * - Gestion de codigos de respaldo (backup codes)
 * - Auditoria de eventos MFA
 * - Bloqueo por intentos fallidos
 *
 * Tests: 45 tests en mfa.service.spec.ts
 */

// Configuracion
const BACKUP_CODES_COUNT = 10;
const MAX_FAILED_ATTEMPTS = 5;
const LOCKOUT_DURATION_MINUTES = 15;
const TOTP_WINDOW = 1; // Tolerancia de 1 paso para drift de tiempo

export interface MfaRequestContext {
  ipAddress: string;
  userAgent: string;
  deviceFingerprint?: string;
}

@Injectable()
export class MfaService {
  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @InjectRepository(UserMfa) private readonly userMfaRepository: Repository<UserMfa>,
    @InjectRepository(MfaAuditLog) private readonly mfaAuditLogRepository: Repository<MfaAuditLog>,
  ) {}

  /**
   * Inicia configuracion MFA para un usuario
   * Genera secret TOTP, QR code y codigos de respaldo
   */
  async initiateMfaSetup(userId: string, context: MfaRequestContext): Promise<MfaSetupResponseDto> {
    // Genera secret TOTP
    const secret = authenticator.generateSecret();
    const otpauthUri = authenticator.keyuri(user.email, 'ERP Generic', secret);
    const qrCodeDataUrl = await QRCode.toDataURL(otpauthUri);

    // Genera codigos de respaldo (10 codigos formato XXXX-XXXX)
    const { codes, hashes } = await this.generateBackupCodes();

    // Crea/actualiza registro con estado PENDING_SETUP
    // ...

    return { secret, otpauthUri, qrCodeDataUrl, backupCodes: codes };
  }

  /**
   * Habilita MFA despues de verificar codigo de configuracion
   */
  async enableMfa(userId: string, code: string, context: MfaRequestContext): Promise<MfaEnabledResponseDto>;

  /**
   * Verifica codigo TOTP o codigo de respaldo
   * Incrementa intentos fallidos y bloquea si excede limite
   */
  async verifyTotp(userId: string, code: string, context: MfaRequestContext): Promise<VerifyTotpResponseDto>;

  /**
   * Deshabilita MFA (requiere password + codigo TOTP/backup)
   */
  async disableMfa(userId: string, password: string, code: string, context: MfaRequestContext): Promise<MfaDisabledResponseDto>;

  /**
   * Regenera codigos de respaldo (requiere verificacion TOTP)
   */
  async regenerateBackupCodes(userId: string, code: string, context: MfaRequestContext): Promise<BackupCodesResponseDto>;

  /**
   * Obtiene estado MFA de un usuario
   */
  async getMfaStatus(userId: string): Promise<MfaStatusResponseDto>;

  /**
   * Verifica si el usuario tiene MFA habilitado
   */
  async isMfaEnabled(userId: string): Promise<boolean>;
}
```

### TrustedDevicesService

```typescript
// services/trusted-devices.service.ts
/**
 * Servicio de Gestion de Dispositivos Confiables
 *
 * Permite bypass de MFA para dispositivos previamente autenticados.
 * Niveles de confianza: STANDARD (30d), HIGH (90d), TEMPORARY (24h)
 *
 * Tests: 41 tests en trusted-devices.service.spec.ts
 */

const DEFAULT_TRUST_DURATION_DAYS = 30;
const HIGH_TRUST_DURATION_DAYS = 90;
const TEMPORARY_TRUST_DURATION_HOURS = 24;
const MAX_TRUSTED_DEVICES_PER_USER = 10;

export interface DeviceInfo {
  userAgent: string;
  deviceName?: string;
  deviceType?: string;
  browserName?: string;
  osName?: string;
}

@Injectable()
export class TrustedDevicesService {
  constructor(
    @InjectRepository(TrustedDevice) private readonly trustedDeviceRepository: Repository<TrustedDevice>,
    @InjectRepository(User) private readonly userRepository: Repository<User>,
  ) {}

  /**
   * Genera fingerprint del dispositivo (hash SHA256 de userAgent + IP)
   */
  generateFingerprint(deviceInfo: DeviceInfo, ipAddress: string): string;

  /**
   * Agrega dispositivo confiable para un usuario
   * Si excede limite (10), elimina el mas antiguo
   */
  async addDevice(
    userId: string,
    deviceInfo: DeviceInfo,
    context: RequestContext,
    trustLevel: TrustLevel = TrustLevel.STANDARD
  ): Promise<AddDeviceResult>;

  /**
   * Verifica si un dispositivo es confiable
   * Actualiza last_used_at y use_count
   */
  async verifyDevice(userId: string, deviceInfo: DeviceInfo, ipAddress: string): Promise<boolean>;

  /**
   * Lista todos los dispositivos confiables de un usuario
   */
  async listDevices(userId: string, currentFingerprint?: string): Promise<TrustedDeviceDto[]>;

  /**
   * Revoca un dispositivo especifico
   */
  async revokeDevice(userId: string, deviceId: string, reason?: string): Promise<void>;

  /**
   * Revoca todos los dispositivos de un usuario (seguridad)
   */
  async revokeAllDevices(userId: string, reason?: string): Promise<number>;

  /**
   * Limpieza de dispositivos expirados (cron job)
   */
  async cleanupExpiredDevices(): Promise<number>;

  /**
   * Actualiza nivel de confianza de un dispositivo
   */
  async updateTrustLevel(userId: string, deviceId: string, trustLevel: TrustLevel): Promise<TrustedDevice>;
}
```

### EmailVerificationService

```typescript
// services/email-verification.service.ts
/**
 * Servicio de Verificacion de Email
 *
 * Maneja el flujo de verificacion de email para nuevos usuarios:
 * - Generacion de tokens seguros (crypto.randomBytes)
 * - Envio de emails de verificacion
 * - Verificacion de tokens y activacion de cuentas
 * - Rate limiting para reenvios (2 min cooldown)
 *
 * Tests: 32 tests en email-verification.service.spec.ts
 */

@Injectable()
export class EmailVerificationService {
  private readonly TOKEN_EXPIRY_HOURS = 24;
  private readonly RESEND_COOLDOWN_MINUTES = 2;

  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @InjectRepository(EmailVerificationToken) private readonly tokenRepository: Repository<EmailVerificationToken>,
    private readonly emailService: EmailService,
  ) {}

  /**
   * Genera token de verificacion seguro (32 bytes hex)
   */
  generateVerificationToken(): string {
    return crypto.randomBytes(32).toString('hex');
  }

  /**
   * Envia email de verificacion al usuario
   * Invalida tokens anteriores antes de crear uno nuevo
   */
  async sendVerificationEmail(userId: string, email: string, ipAddress?: string): Promise<SendVerificationResponse>;

  /**
   * Verifica email con token
   * Marca token como usado y activa usuario si estaba PENDING_VERIFICATION
   */
  async verifyEmail(token: string, ipAddress?: string): Promise<VerifyEmailResponse>;

  /**
   * Reenvia email de verificacion con rate limiting
   */
  async resendVerificationEmail(userId: string, ipAddress?: string): Promise<SendVerificationResponse>;

  /**
   * Obtiene estado de verificacion de email
   */
  async getVerificationStatus(userId: string): Promise<EmailVerificationStatusResponse>;

  /**
   * Limpieza de tokens expirados (cron job)
   */
  async cleanupExpiredTokens(): Promise<number>;
}
```

### PermissionCacheService

```typescript
// services/permission-cache.service.ts
/**
 * Servicio de Cache de Permisos en Redis
 *
 * Proporciona lookups rapidos de permisos con fallback graceful
 * cuando Redis no esta disponible.
 *
 * Tests: 37 tests en permission-cache.service.spec.ts
 */

@Injectable()
export class PermissionCacheService {
  readonly DEFAULT_TTL = 300; // 5 minutos
  readonly KEY_PREFIX = 'perm:';

  constructor(@InjectRedis() private readonly redis: Redis) {}

  // ===== Cache de Permisos =====

  /**
   * Obtiene permisos cacheados para un usuario
   */
  async getUserPermissions(userId: string): Promise<string[] | null>;

  /**
   * Cachea permisos de un usuario
   */
  async setUserPermissions(userId: string, permissions: string[], ttl?: number): Promise<void>;

  /**
   * Invalida cache de permisos de un usuario
   */
  async invalidateUserPermissions(userId: string): Promise<void>;

  // ===== Cache de Roles =====

  /**
   * Obtiene roles cacheados para un usuario
   */
  async getUserRoles(userId: string): Promise<string[] | null>;

  /**
   * Cachea roles de un usuario
   */
  async setUserRoles(userId: string, roles: string[], ttl?: number): Promise<void>;

  /**
   * Invalida cache de roles de un usuario
   */
  async invalidateUserRoles(userId: string): Promise<void>;

  // ===== Verificacion de Permisos =====

  /**
   * Verifica si usuario tiene un permiso especifico
   */
  async hasPermission(userId: string, permission: string): Promise<boolean>;

  /**
   * Verifica si usuario tiene alguno de los permisos
   */
  async hasAnyPermission(userId: string, permissions: string[]): Promise<boolean>;

  /**
   * Verifica si usuario tiene todos los permisos
   */
  async hasAllPermissions(userId: string, permissions: string[]): Promise<boolean>;

  // ===== Invalidacion Masiva =====

  /**
   * Invalida todo el cache (permisos y roles) de un usuario
   */
  async invalidateAllForUser(userId: string): Promise<void>;

  /**
   * Invalida todo el cache de un tenant (usando SCAN)
   */
  async invalidateAllForTenant(tenantId: string): Promise<void>;
}
```

### ApiKeysService

```typescript
// apiKeys.service.ts
/**
 * Servicio de Gestion de API Keys
 *
 * Permite autenticacion programatica mediante API keys:
 * - Generacion de keys seguras con PBKDF2
 * - Validacion con whitelist de IPs
 * - Expiracion configurable
 * - Scopes para control de acceso
 */

const API_KEY_PREFIX = 'mgn_';
const KEY_LENGTH = 32; // 256 bits
const HASH_ITERATIONS = 100000;
const HASH_DIGEST = 'sha512';

export interface CreateApiKeyDto {
  user_id: string;
  tenant_id: string;
  name: string;
  scope?: string;
  allowed_ips?: string[];
  expiration_days?: number;
}

@Injectable()
export class ApiKeysService {
  /**
   * Crea una nueva API key
   * La key plain solo se retorna una vez, no puede recuperarse
   */
  async create(dto: CreateApiKeyDto): Promise<ApiKeyWithPlainKey>;

  /**
   * Lista todas las API keys de un usuario/tenant
   */
  async findAll(filters: ApiKeyFilters): Promise<ApiKey[]>;

  /**
   * Busca API key por ID
   */
  async findById(id: string, tenantId: string): Promise<ApiKey | null>;

  /**
   * Actualiza una API key
   */
  async update(id: string, tenantId: string, dto: UpdateApiKeyDto): Promise<ApiKey>;

  /**
   * Revoca (soft delete) una API key
   */
  async revoke(id: string, tenantId: string): Promise<void>;

  /**
   * Elimina permanentemente una API key
   */
  async delete(id: string, tenantId: string): Promise<void>;

  /**
   * Valida una API key y retorna info del usuario
   * Usado por el middleware de autenticacion
   */
  async validate(plainKey: string, clientIp?: string): Promise<ApiKeyValidationResult>;

  /**
   * Regenera una API key (invalida la anterior)
   */
  async regenerate(id: string, tenantId: string): Promise<ApiKeyWithPlainKey>;
}
```

### OAuthService

```typescript
// providers/oauth.service.ts
/**
 * Servicio de Autenticacion OAuth2
 *
 * Soporta Google y Microsoft con:
 * - PKCE para seguridad adicional
 * - State management para proteccion CSRF
 * - Creacion/vinculacion automatica de usuarios
 * - Configuracion por tenant
 *
 * Tests: 32 tests en oauth.service.spec.ts
 */

const STATE_EXPIRY_MINUTES = 10;

export type OAuthProviderType = 'google' | 'microsoft';

export interface OAuthLoginResult {
  isNewUser: boolean;
  userId: string;
  tenantId: string;
  accessToken: string;
  refreshToken: string;
  sessionId: string;
  expiresAt: Date;
}

@Injectable()
export class OAuthService {
  private providers: Map<OAuthProviderType, IOAuthProvider> = new Map();

  constructor(
    @InjectRepository(User) private readonly userRepository: Repository<User>,
    @InjectRepository(OAuthProvider) private readonly providerRepository: Repository<OAuthProvider>,
    @InjectRepository(OAuthUserLink) private readonly userLinkRepository: Repository<OAuthUserLink>,
    @InjectRepository(OAuthState) private readonly stateRepository: Repository<OAuthState>,
    @InjectRepository(Tenant) private readonly tenantRepository: Repository<Tenant>,
    private readonly tokenService: TokenService,
  ) {}

  // ===== PKCE Helpers =====

  private generateCodeVerifier(): string {
    return crypto.randomBytes(32).toString('base64url');
  }

  private generateCodeChallenge(verifier: string): string {
    return crypto.createHash('sha256').update(verifier).digest('base64url');
  }

  // ===== OAuth Flow =====

  /**
   * Inicia flujo OAuth - genera URL de autorizacion
   * Guarda state con PKCE code_verifier en BD
   */
  async initiateOAuth(
    providerType: OAuthProviderType,
    returnUrl: string | undefined,
    metadata: RequestMetadata,
    linkUserId?: string
  ): Promise<{ authUrl: string; state: string }>;

  /**
   * Maneja callback OAuth
   * Valida state, intercambia code por tokens, crea/vincula usuario
   */
  async handleCallback(
    providerType: OAuthProviderType,
    code: string,
    state: string,
    metadata: RequestMetadata
  ): Promise<OAuthLoginResult>;

  // ===== Gestion de Cuentas =====

  /**
   * Obtiene links OAuth de un usuario
   */
  async getUserOAuthLinks(userId: string): Promise<OAuthUserLink[]>;

  /**
   * Desvincula proveedor OAuth de un usuario
   * No permite si es el unico metodo de autenticacion
   */
  async unlinkProvider(userId: string, providerId: string): Promise<void>;

  /**
   * Limpieza de estados OAuth expirados (cron job)
   */
  async cleanupExpiredStates(): Promise<number>;
}
```

---

## Controller

```typescript
// controllers/auth.controller.ts
import {
  Controller, Post, Get, Body, Param, Req, Res,
  HttpCode, HttpStatus, UseGuards
} from '@nestjs/common';
import { Response, Request } from 'express';
import { ApiTags, ApiOperation, ApiResponse, ApiBearerAuth } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { AuthService } from '../services/auth.service';
import { TokenService } from '../services/token.service';
import { MfaService } from '../mfa.service';
import { TrustedDevicesService } from '../services/trusted-devices.service';
import { EmailVerificationService } from '../services/email-verification.service';
import { ApiKeysService } from '../apiKeys.service';
import { OAuthService } from '../providers/oauth.service';
import { LoginDto } from '../dto/login.dto';
import { LoginResponseDto } from '../dto/login-response.dto';
import { RefreshTokenDto } from '../dto/refresh-token.dto';
import { TokenResponseDto } from '../dto/token-response.dto';
import { RequestPasswordResetDto } from '../dto/request-password-reset.dto';
import { ResetPasswordDto } from '../dto/reset-password.dto';
import { JwtAuthGuard } from '../guards/jwt-auth.guard';
import { Public } from '../decorators/public.decorator';
import { CurrentUser } from '../decorators/current-user.decorator';

@ApiTags('Auth')
@Controller('api/v1/auth')
export class AuthController {
  constructor(
    private readonly authService: AuthService,
    private readonly tokenService: TokenService,
    private readonly mfaService: MfaService,
    private readonly trustedDevicesService: TrustedDevicesService,
    private readonly emailVerificationService: EmailVerificationService,
    private readonly apiKeysService: ApiKeysService,
    private readonly oauthService: OAuthService,
  ) {}

  @Post('login')
  @Public()
  @Throttle({ default: { limit: 10, ttl: 60000 } }) // 10 requests/min
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Autenticar usuario' })
  @ApiResponse({ status: 200, type: LoginResponseDto })
  @ApiResponse({ status: 401, description: 'Credenciales inválidas' })
  @ApiResponse({ status: 423, description: 'Cuenta bloqueada' })
  async login(
    @Body() dto: LoginDto,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<LoginResponseDto> {
    const metadata = {
      ipAddress: req.ip,
      userAgent: req.headers['user-agent'] || '',
    };

    const result = await this.authService.login(dto, metadata);

    // Set refresh token as httpOnly cookie
    res.cookie('refresh_token', result.refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000, // 7 días
    });

    return result;
  }

  @Post('refresh')
  @Public()
  @Throttle({ default: { limit: 1, ttl: 1000 } }) // 1 request/sec
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Renovar tokens' })
  @ApiResponse({ status: 200, type: TokenResponseDto })
  @ApiResponse({ status: 401, description: 'Token inválido o expirado' })
  async refresh(
    @Body() dto: RefreshTokenDto,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<TokenResponseDto> {
    // Preferir cookie sobre body
    const refreshToken = req.cookies['refresh_token'] || dto.refreshToken;

    const tokens = await this.tokenService.refreshTokens(refreshToken);

    // Update cookie
    res.cookie('refresh_token', tokens.refreshToken, {
      httpOnly: true,
      secure: process.env.NODE_ENV === 'production',
      sameSite: 'strict',
      maxAge: 7 * 24 * 60 * 60 * 1000,
    });

    return {
      accessToken: tokens.accessToken,
      refreshToken: tokens.refreshToken,
      tokenType: 'Bearer',
      expiresIn: 900,
    };
  }

  @Post('logout')
  @UseGuards(JwtAuthGuard)
  @HttpCode(HttpStatus.OK)
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Cerrar sesión' })
  @ApiResponse({ status: 200, description: 'Sesión cerrada' })
  async logout(
    @CurrentUser() user: any,
    @Req() req: Request,
    @Res({ passthrough: true }) res: Response,
  ): Promise<{ message: string }> {
    const refreshToken = req.cookies['refresh_token'];
    const accessTokenJti = user.jti;

    await this.authService.logout(
      user.sub,
      user.tid,
      refreshToken, // Extraer jti del refresh token
      accessTokenJti,
    );

    // Clear cookie
    res.clearCookie('refresh_token');

    return { message: 'Sesión cerrada exitosamente' };
  }

  @Post('logout-all')
  @UseGuards(JwtAuthGuard)
  @Throttle({ default: { limit: 5, ttl: 60000 } })
  @HttpCode(HttpStatus.OK)
  @ApiBearerAuth()
  @ApiOperation({ summary: 'Cerrar todas las sesiones' })
  @ApiResponse({ status: 200, description: 'Todas las sesiones cerradas' })
  async logoutAll(
    @CurrentUser() user: any,
    @Res({ passthrough: true }) res: Response,
  ): Promise<{ message: string; sessionsRevoked: number }> {
    const count = await this.authService.logoutAll(user.sub, user.tid);

    res.clearCookie('refresh_token');

    return {
      message: 'Todas las sesiones han sido cerradas',
      sessionsRevoked: count,
    };
  }

  @Post('password/request-reset')
  @Public()
  @Throttle({ default: { limit: 3, ttl: 3600000 } }) // 3/hora
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Solicitar recuperación de contraseña' })
  @ApiResponse({ status: 200, description: 'Email enviado si existe' })
  async requestPasswordReset(
    @Body() dto: RequestPasswordResetDto,
  ): Promise<{ message: string }> {
    await this.passwordService.requestPasswordReset(dto.email);
    return {
      message: 'Si el email está registrado, recibirás instrucciones para restablecer tu contraseña',
    };
  }

  @Get('password/validate-token/:token')
  @Public()
  @Throttle({ default: { limit: 10, ttl: 60000 } })
  @ApiOperation({ summary: 'Validar token de recuperación' })
  @ApiResponse({ status: 200 })
  async validateResetToken(
    @Param('token') token: string,
  ): Promise<{ valid: boolean; email?: string; reason?: string }> {
    return this.passwordService.validateResetToken(token);
  }

  @Post('password/reset')
  @Public()
  @Throttle({ default: { limit: 5, ttl: 3600000 } }) // 5/hora
  @HttpCode(HttpStatus.OK)
  @ApiOperation({ summary: 'Restablecer contraseña' })
  @ApiResponse({ status: 200, description: 'Contraseña actualizada' })
  @ApiResponse({ status: 400, description: 'Token inválido o expirado' })
  async resetPassword(
    @Body() dto: ResetPasswordDto,
  ): Promise<{ message: string }> {
    if (dto.newPassword !== dto.confirmPassword) {
      throw new BadRequestException('Las contraseñas no coinciden');
    }

    await this.passwordService.resetPassword(dto.token, dto.newPassword);

    return {
      message: 'Contraseña actualizada exitosamente. Por favor inicia sesión.',
    };
  }
}
```

---

## Guards y Decorators

### JwtAuthGuard

```typescript
// guards/jwt-auth.guard.ts
import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { Reflector } from '@nestjs/core';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
import { TokenService } from '../services/token.service';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(
    private reflector: Reflector,
    private tokenService: TokenService,
  ) {
    super();
  }

  async canActivate(context: ExecutionContext): Promise<boolean> {
    // Check if route is public
    const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
      context.getHandler(),
      context.getClass(),
    ]);

    if (isPublic) {
      return true;
    }

    // Run passport strategy
    const canActivate = await super.canActivate(context);
    if (!canActivate) {
      return false;
    }

    // Verificar que el token no haya sido revocado
    const request = context.switchToHttp().getRequest();
    const user = request.user;

    // La validacion de tokens revocados se hace en el TokenService
    // mediante consulta a la BD de refresh_tokens

    return true;
  }

  handleRequest(err: any, user: any, info: any) {
    if (err || !user) {
      throw err || new UnauthorizedException('Token invalido o expirado');
    }
    return user;
  }
}
```

### Public Decorator

```typescript
// decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
```

### CurrentUser Decorator

```typescript
// decorators/current-user.decorator.ts
import { createParamDecorator, ExecutionContext } from '@nestjs/common';

export const CurrentUser = createParamDecorator(
  (data: string, ctx: ExecutionContext) => {
    const request = ctx.switchToHttp().getRequest();
    const user = request.user;
    return data ? user?.[data] : user;
  },
);
```

---

## Configuracion

### JWT Config

```typescript
// config/jwt.config.ts
import { registerAs } from '@nestjs/config';

export default registerAs('jwt', () => ({
  accessToken: {
    algorithm: 'RS256',
    expiresIn: '15m',
    issuer: 'erp-core',
    audience: 'erp-api',
  },
  refreshToken: {
    algorithm: 'RS256',
    expiresIn: '7d',
    issuer: 'erp-core',
    audience: 'erp-api',
  },
  privateKey: process.env.JWT_PRIVATE_KEY,
  publicKey: process.env.JWT_PUBLIC_KEY,
}));
```

### Auth Module

```typescript
// auth.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { ThrottlerModule } from '@nestjs/throttler';
import { AuthController } from './controllers/auth.controller';
import { AuthService } from './services/auth.service';
import { TokenService } from './services/token.service';
import { MfaService } from './mfa.service';
import { TrustedDevicesService } from './services/trusted-devices.service';
import { EmailVerificationService } from './services/email-verification.service';
import { PermissionCacheService } from './services/permission-cache.service';
import { ApiKeysService } from './apiKeys.service';
import { OAuthService } from './providers/oauth.service';
import { JwtStrategy } from './strategies/jwt.strategy';
import { JwtAuthGuard } from './guards/jwt-auth.guard';
// Entidades
import { RefreshToken } from './entities/refresh-token.entity';
import { RevokedToken } from './entities/revoked-token.entity';
import { SessionHistory } from './entities/session-history.entity';
import { LoginAttempt } from './entities/login-attempt.entity';
import { PasswordResetToken } from './entities/password-reset-token.entity';
import { PasswordHistory } from './entities/password-history.entity';
import { UserMfa } from './entities/user-mfa.entity';
import { MfaAuditLog } from './entities/mfa-audit-log.entity';
import { TrustedDevice } from './entities/trusted-device.entity';
import { EmailVerificationToken } from './entities/email-verification-token.entity';
import { ApiKey } from './entities/api-key.entity';
import { OAuthProvider } from './entities/oauth-provider.entity';
import { OAuthState } from './entities/oauth-state.entity';
import { OAuthUserLink } from './entities/oauth-user-link.entity';

@Module({
  imports: [
    TypeOrmModule.forFeature([
      RefreshToken,
      RevokedToken,
      SessionHistory,
      LoginAttempt,
      PasswordResetToken,
      PasswordHistory,
      UserMfa,
      MfaAuditLog,
      TrustedDevice,
      EmailVerificationToken,
      ApiKey,
      OAuthProvider,
      OAuthState,
      OAuthUserLink,
    ]),
    PassportModule.register({ defaultStrategy: 'jwt' }),
    JwtModule.register({}),
    ThrottlerModule.forRoot([{
      ttl: 60000,
      limit: 10,
    }]),
  ],
  controllers: [AuthController],
  providers: [
    AuthService,
    TokenService,
    MfaService,
    TrustedDevicesService,
    EmailVerificationService,
    PermissionCacheService,
    ApiKeysService,
    OAuthService,
    JwtStrategy,
    JwtAuthGuard,
  ],
  exports: [
    AuthService,
    TokenService,
    MfaService,
    TrustedDevicesService,
    EmailVerificationService,
    PermissionCacheService,
    ApiKeysService,
    OAuthService,
    JwtAuthGuard,
  ],
})
export class AuthModule {}
```

---

## Manejo de Errores

### Error Responses

```typescript
// Estructura estándar de error
{
  "statusCode": 401,
  "message": "Credenciales inválidas",
  "error": "Unauthorized",
  "timestamp": "2025-12-05T10:30:00.000Z",
  "path": "/api/v1/auth/login"
}
```

### Códigos de Error

| Código | Constante | Descripción |
|--------|-----------|-------------|
| AUTH001 | INVALID_CREDENTIALS | Email o password incorrecto |
| AUTH002 | ACCOUNT_LOCKED | Cuenta bloqueada por intentos |
| AUTH003 | ACCOUNT_INACTIVE | Cuenta deshabilitada |
| AUTH004 | TOKEN_EXPIRED | Token expirado |
| AUTH005 | TOKEN_INVALID | Token malformado o inválido |
| AUTH006 | TOKEN_REVOKED | Token revocado |
| AUTH007 | SESSION_COMPROMISED | Reuso de token detectado |
| AUTH008 | RESET_TOKEN_EXPIRED | Token de reset expirado |
| AUTH009 | RESET_TOKEN_USED | Token de reset ya usado |
| AUTH010 | PASSWORD_REUSED | Password igual a anterior |

---

## Notas de Implementacion

1. **Seguridad:**
   - Siempre usar HTTPS en producción
   - Refresh token SOLO en httpOnly cookie
   - Rate limiting en todos los endpoints
   - No revelar existencia de emails

2. **Performance:**
   - Blacklist en Redis (no en PostgreSQL)
   - Índices apropiados en tablas de tokens
   - Connection pooling para BD

3. **Monitoreo:**
   - Loguear todos los eventos de auth
   - Alertas en detección de token replay
   - Métricas de intentos fallidos

---

## Historial de Cambios

| Version | Fecha | Autor | Cambios |
|---------|-------|-------|---------|
| 1.0 | 2025-12-05 | System | Creacion inicial |
| 2.0 | 2026-01-10 | System | Documentacion de servicios implementados: MfaService (45 tests), TrustedDevicesService (41 tests), EmailVerificationService (32 tests), PermissionCacheService (37 tests), ApiKeysService, OAuthService (32 tests). Eliminacion de referencias a servicios inexistentes (password.service.ts, blacklist.service.ts). Adicion de nuevas entidades: UserMfa, TrustedDevice, EmailVerificationToken, ApiKey, OAuthProvider. |

---

## Aprobaciones

| Rol | Nombre | Fecha | Firma |
|-----|--------|-------|-------|
| Tech Lead | - | - | [ ] |
| Security | - | - | [ ] |
| Backend Lead | - | - | [ ] |
