Security Updated Plan

Ecosistema completo para organización y gestión de contenido con interfaces espaciales

Librería Security (Core) - Plan Actualizado

Propósito: Proporcionar autenticación, autorización, multitenancy completo y protección de seguridad para aplicaciones web del lado del cliente usando Angular 20.

Estado Planificado

Arquitectura de Seguridad y Multitenancy

La librería implementa un sistema de seguridad robusto con soporte nativo de multitenancy, aprovechando Angular 20 con signals, standalone components, y httpResource para manejo seguro de autenticación y contexto de tenant.

Arquitectura Multitenancy (Cliente Angular)

Cambios Arquitectónicos Principales

API Pública Planificada

Authentication & Multitenancy Layer

Authorization Layer (RBAC + Multitenancy)

Multitenancy Core (Nueva Sección)

Guards y Resolvers (Angular 20 + Multitenancy)

HTTP Security Layer (Multitenancy Ready)

Estructura del Paquete (Actualizada)

src/lib/
├── auth/
│   ├── auth.service.ts                    # AuthService con tenant context
│   ├── token.service.ts                   # TokenService por tenant
│   ├── session.service.ts                 # SessionService multitenancy
│   └── biometric-auth.service.ts          # Autenticación biométrica
├── multitenancy/                          # ← NUEVA SECCIÓN
│   ├── models/
│   │   ├── context.ts                     # Context interface
│   │   ├── config.ts                      # Config interface
│   │   ├── permissions.ts                 # Permissions interface
│   │   └── api-config.ts                  # ApiConfig
│   ├── services/
│   │   ├── context.service.ts             # Gestión contexto
│   │   ├── config.service.ts              # Configuración
│   │   ├── api-url.service.ts             # URLs API
│   │   └── context-validator.service.ts   # Validación contexto
│   ├── interceptors/
│   │   ├── context.interceptor.ts         # X-Tenant-ID headers
│   │   └── csrf.interceptor.ts            # CSRF
│   ├── guards/
│   │   ├── context.guard.ts               # Guard contexto
│   │   └── permission.guard.ts            # Guard permisos
│   ├── tokens/
│   │   ├── context.token.ts               # CONTEXT_TOKEN
│   │   ├── context-id.token.ts            # CONTEXT_ID_TOKEN
│   │   └── api-base-url.token.ts          # API_BASE_URL_TOKEN
│   └── resolvers/
│       └── context.resolver.ts            # Resolver contexto
├── authorization/
│   ├── permission.service.ts              # PermissionService
│   ├── role.service.ts                    # RoleService
│   ├── module-permission.service.ts       # Permisos módulos
│   └── permission-context.service.ts      # Permisos específicos
├── guards/
│   ├── auth.guard.ts                      # AuthGuard
│   ├── role.guard.ts                      # RoleGuard
│   ├── permission.guard.ts                # PermissionGuard
│   └── security.resolver.ts               # SecurityResolver
├── interceptors/
│   ├── auth.interceptor.ts                # AuthInterceptor
│   ├── csrf.interceptor.ts                # CSRFInterceptor
│   ├── security-headers.interceptor.ts    # SecurityHeadersInterceptor
│   └── rate-limit.interceptor.ts          # RateLimitInterceptor
├── browser-security/
│   ├── csp.service.ts                     # CSPService
│   ├── xss-protection.service.ts          # XSSProtectionService
│   ├── secure-storage.service.ts          # SecureStorageService
│   └── fingerprinting.service.ts          # FingerprintingService
├── models/
│   ├── security-config.ts                 # SecurityConfig específico
│   └── index.ts                           # Re-exports de shared-models
├── directives/
│   ├── has-permission.directive.ts        # HasPermissionDirective
│   ├── has-role.directive.ts              # HasRoleDirective
│   ├── context-aware.directive.ts         # ContextAwareDirective (nuevo)
│   └── secure-content.directive.ts        # SecureContentDirective
└── utils/
    ├── crypto.utils.ts                    # CryptoUtils
    ├── jwt.utils.ts                       # JWTUtils
    ├── security-validators.ts             # SecurityValidators
    ├── context.utils.ts                   # ContextUtils (nuevo)
    └── device-detector.ts                 # DeviceDetector

Funcionalidad Clave con Angular 20 + Multitenancy

1. AuthService con Contexto (usando shared-models)

import { Context, UserProfile, AuthState, LoginCredentials, LoginResult, ContextInfo, Permission } from '@libs-core/shared-models';

@Injectable({ providedIn: 'root' })
export class AuthService {
  // Signals para estado reactivo con tipos de shared-models
  readonly isAuthenticated = signal<boolean>(false);
  readonly currentUser = signal<UserProfile | null>(null);
  readonly currentContext = signal<Context | null>(null);
  readonly authState = signal<AuthState>('idle');
  readonly availableContexts = signal<ContextInfo[]>([]);

  // httpResource para autenticación
  private readonly authResource = httpResource({
    url: '/api/auth/login',
    loader: (params: LoginCredentials) => this.httpClient.post<LoginResult>('/api/auth/login', params),
  });

  constructor(private httpClient: HttpClient, private tokenService: TokenService, private contextService: ContextService, private secureStorage: SecureStorageService) {
    this.initializeAuthState();
  }

  async login(credentials: LoginCredentials): Promise<AuthResult> {
    this.authState.set('authenticating');

    try {
      const result = await this.authResource.load(credentials);

      // Almacenar tokens de forma segura
      await this.tokenService.storeTokens(result.accessToken, result.refreshToken);

      // Establecer contexto de tenant
      await this.setTenantContext(result.tenantContext);

      // Actualizar estado con signals
      this.isAuthenticated.set(true);
      this.currentUser.set(result.user);
      this.authState.set('authenticated');

      return result;
    } catch (error) {
      this.authState.set('error');
      throw new AuthenticationError(error);
    }
  }

  async setTenantContext(tenantContext: TenantContext): Promise<void> {
    this.currentTenantContext.set(tenantContext);
    await this.tenantContextService.switchContext(tenantContext);
  }

  // Métodos para integración con module-base
  getCurrentTenantContext(): TenantContext | null {
    return this.currentTenantContext();
  }

  hasPermission(permission: string): boolean {
    const tenantContext = this.currentTenantContext();
    const userPermissions = this.userPermissions();
    return tenantContext && userPermissions.includes(permission);
  }

  hasModulePermission(moduleId: string): boolean {
    const tenantContext = this.currentTenantContext();
    if (!tenantContext) return false;

    return tenantContext.permissions.modules[moduleId]?.enabled ?? false;
  }

  getApiBaseUrl(): string | null {
    const tenantContext = this.currentTenantContext();
    return tenantContext?.apiBaseUrl ?? null;
  }

  // Computed signals para permisos por tenant
  readonly userPermissions = computed(() => {
    const user = this.currentUser();
    if (!user) return [];

    // Los permisos vienen directamente del usuario almacenado en la BD del tenant
    return user.permissions.map((permission) => permission.name);
  });

  readonly userRoles = computed(() => {
    const user = this.currentUser();
    if (!user) return [];

    // Los roles vienen de la BD del tenant
    return user.roles.map((role) => role.name);
  });

  readonly canAccessResource = (resource: string, action: string) =>
    computed(() => {
      const user = this.currentUser();
      if (!user) return false;

      // Verificar permisos específicos del tenant
      return user.permissions.some((permission) => permission.resource === resource && permission.action === action && permission.tenantId === user.tenantId);
    });
}

2. Modelos de Multitenancy (desde @libs-core/shared-models)

La librería security utiliza todos los modelos de @libs-core/shared-models para garantizar consistencia:

// Imports desde shared-models
import {
  // Tenant domain
  Context,
  Config,
  Permissions,
  Features,
  ContextInfo,

  // Auth domain
  UserProfile,
  AuthState,
  LoginCredentials,
  LoginResult,
  SelectionResult,

  // RBAC domain
  Role,
  Permission,
  PermissionScope,
  PermissionCondition,

  // UI domain
  BrandingConfig,
  ModuleConfig,
  UserPreferences,

  // API domain
  ApiResponse,
  PaginationRequest,
  ErrorResponse,
} from '@libs-core/shared-models';

// Ejemplo de uso en AuthService
@Injectable({ providedIn: 'root' })
export class AuthService {
  // Signals tipados con modelos compartidos
  readonly currentUser = signal<UserProfile | null>(null);
  readonly currentContext = signal<Context | null>(null);
  readonly authState = signal<AuthState>('idle');
  readonly availableContexts = signal<ContextInfo[]>([]);

  async login(credentials: LoginCredentials): Promise<LoginResult> {
    // Implementación usando tipos compartidos
  }

  getCurrentContext(): Context | null {
    return this.currentContext();
  }

  hasPermission(permission: string): boolean {
    const user = this.currentUser();
    return user?.permissions.some((p: Permission) => p.name === permission) ?? false;
  }
}

Beneficios de usar shared-models:

3. ContextInterceptor para Headers Automáticos

@Injectable()
export class ContextInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const context = this.authService.getCurrentContext();

    if (context && !this.isPublicRoute(req.url)) {
      const modifiedReq = req.clone({
        setHeaders: {
          'X-Tenant-ID': context.tenantId,
          'X-Tenant-Timezone': context.timezone,
          'X-Tenant-Locale': context.locale,
        },
        url: this.buildContextUrl(req.url, context),
      });

      return next.handle(modifiedReq);
    }

    return next.handle(req);
  }

  private buildContextUrl(originalUrl: string, context: Context): string {
    // Si es una URL relativa, usar la URL base del tenant
    if (originalUrl.startsWith('/api') && context.apiBaseUrl) {
      return `${context.apiBaseUrl}${originalUrl}`;
    }
    return originalUrl;
  }

  private isPublicRoute(url: string): boolean {
    // Rutas que no requieren contexto de tenant
    const publicRoutes = [
      '/api/auth/login', // Login inicial
      '/api/health', // Health check
      '/api/tenants/list', // Lista de tenants disponibles
      '/api/system', // APIs del sistema
    ];
    return publicRoutes.some((route) => url.includes(route));
  }
}

4. ContextService para Gestión de Contexto

@Injectable({ providedIn: 'root' })
export class ContextService {
  private readonly currentContext = signal<Context | null>(null);
  private readonly contextHistory = signal<Context[]>([]);

  constructor(private httpClient: HttpClient, private secureStorage: SecureStorageService, private eventSystem: EventSystem) {
    this.loadStoredContext();
  }

  getCurrentContext(): Signal<Context | null> {
    return this.currentContext.asReadonly();
  }

  async switchContext(context: Context): Promise<void> {
    // Almacenar contexto anterior en historial
    const currentCtx = this.currentContext();
    if (currentCtx) {
      const history = this.contextHistory();
      this.contextHistory.set([currentCtx, ...history.slice(0, 4)]); // Mantener últimos 5
    }

    // Establecer nuevo contexto
    this.currentContext.set(context);

    // Almacenar de forma segura
    await this.secureStorage.setSecure('context', context);

    // Emitir evento de cambio de contexto
    this.eventSystem.emit({
      type: 'context-changed',
      payload: {
        previousTenant: currentCtx?.tenantId,
        newTenant: context.tenantId,
      },
    });
  }

  async loadConfig(tenantId: string): Promise<Config> {
    return this.httpClient.get<Config>(`/api/tenants/${tenantId}/config`).toPromise();
  }

  // Computed signals para derivaciones
  readonly currentTenantId = computed(() => this.currentContext()?.tenantId);
  readonly currentApiBaseUrl = computed(() => this.currentContext()?.apiBaseUrl);
  readonly currentPermissions = computed(() => this.currentContext()?.permissions);
}

5. Guards Funcionales con Multitenancy

// Guard para validar contexto
export const contextGuard: CanActivateFn = (route, state) => {
  const authService = inject(AuthService);
  const router = inject(Router);

  const context = authService.getCurrentContext();

  if (!context) {
    return router.createUrlTree(['/select-tenant'], {
      queryParams: { returnUrl: state.url },
    });
  }

  return true;
};

// Guard para permisos de módulos por tenant
export const modulePermissionGuard = (moduleId: string): CanActivateFn => {
  return (route, state) => {
    const authService = inject(AuthService);
    const router = inject(Router);

    if (!authService.hasModulePermission(moduleId)) {
      return router.createUrlTree(['/access-denied'], {
        queryParams: { module: moduleId },
      });
    }

    return true;
  };
};

// Configuración de rutas con guards multitenancy
export const routes: Routes = [
  {
    path: 'app',
    canActivate: [authGuard, contextGuard],
    children: [
      {
        path: 'users',
        canActivate: [modulePermissionGuard('user-management')],
        loadComponent: () => import('./users/users.component').then((m) => m.UsersComponent),
      },
    ],
  },
];

6. Tokens Injectable para Multitenancy

// Tokens para inyección de contexto
export const CONTEXT_TOKEN = new InjectionToken<Signal<Context | null>>('CONTEXT');
export const CONTEXT_ID_TOKEN = new InjectionToken<Signal<string | null>>('CONTEXT_ID');
export const API_BASE_URL_TOKEN = new InjectionToken<Signal<string | null>>('API_BASE_URL');

// Providers para tokens de contexto
export function provideContext(): EnvironmentProviders {
  return makeEnvironmentProviders([
    {
      provide: CONTEXT_TOKEN,
      useFactory: () => {
        const authService = inject(AuthService);
        return computed(() => authService.getCurrentContext());
      },
    },
    {
      provide: CONTEXT_ID_TOKEN,
      useFactory: () => {
        const authService = inject(AuthService);
        return computed(() => authService.getCurrentContext()?.tenantId ?? null);
      },
    },
    {
      provide: API_BASE_URL_TOKEN,
      useFactory: () => {
        const authService = inject(AuthService);
        return computed(() => authService.getCurrentContext()?.apiBaseUrl ?? null);
      },
    },
  ]);
}

Flujo de Autenticación Multitenancy

Proceso de Login con Selección de Tenant

// 1. Login inicial - obtener lista de tenants del usuario
export interface LoginCredentials {
  email: string;
  password: string;
}

export interface LoginResult {
  accessToken: string;
  refreshToken: string;
  user: UserProfile; // Usuario básico sin tenant específico
  availableTenants: ContextInfo[]; // Tenants a los que el usuario tiene acceso
}

export interface ContextInfo {
  tenantId: string;
  tenantName: string;
  role: string; // Rol del usuario en este tenant
  permissions: string[]; // Permisos base en este tenant
}

// 2. Selección de tenant - obtener contexto completo
export interface SelectionRequest {
  tenantId: string;
  accessToken: string;
}

export interface SelectionResult {
  context: Context; // Contexto completo
  userProfile: UserProfile; // Perfil del usuario en este tenant específico
  tenantToken: string; // Token específico para este tenant
}

AuthService Actualizado para Multitenancy con BD

@Injectable({ providedIn: 'root' })
export class AuthService {
  readonly isAuthenticated = signal<boolean>(false);
  readonly currentUser = signal<UserProfile | null>(null);
  readonly currentContext = signal<Context | null>(null);
  readonly availableContexts = signal<ContextInfo[]>([]);
  readonly authState = signal<AuthState>('idle');

  async login(credentials: LoginCredentials): Promise<LoginResult> {
    this.authState.set('authenticating');

    try {
      // 1. Autenticación inicial - sin tenant específico
      const result = await this.httpClient.post<LoginResult>('/api/auth/login', credentials).toPromise();

      // 2. Almacenar información básica
      await this.tokenService.storeTokens(result.accessToken, result.refreshToken);
      this.availableContexts.set(result.availableTenants);
      this.authState.set('context_selection_required');

      return result;
    } catch (error) {
      this.authState.set('error');
      throw new AuthenticationError(error);
    }
  }

  async selectContext(tenantId: string): Promise<void> {
    this.authState.set('loading_context');

    try {
      // 1. Obtener contexto completo
      const result = await this.httpClient
        .post<SelectionResult>('/api/auth/select-tenant', {
          tenantId,
          accessToken: await this.tokenService.getAccessToken(),
        })
        .toPromise();

      // 2. Establecer contexto
      this.currentContext.set(result.context);

      // 3. Establecer usuario específico
      this.currentUser.set(result.userProfile);

      // 4. Almacenar token específico
      await this.tokenService.storeContextToken(result.tenantToken);

      // 5. Actualizar estado
      this.isAuthenticated.set(true);
      this.authState.set('authenticated');

      // 6. Emitir evento de contexto seleccionado
      this.eventSystem.emit({
        type: 'context-selected',
        payload: { tenantId, tenantName: result.context.tenantName },
      });
    } catch (error) {
      this.authState.set('error');
      throw new ContextSelectionError(error);
    }
  }

  async switchContext(tenantId: string): Promise<void> {
    // Verificar que el usuario tiene acceso a este tenant
    const availableContexts = this.availableContexts();
    const hasAccess = availableContexts.some((context) => context.tenantId === tenantId);

    if (!hasAccess) {
      throw new Error('User does not have access to this tenant');
    }

    // Cambiar al nuevo contexto
    await this.selectContext(tenantId);
  }

  // Métodos para integración con module-base
  getCurrentContext(): Context | null {
    return this.currentContext();
  }

  hasPermission(permission: string): boolean {
    const user = this.currentUser();
    if (!user) return false;

    return user.permissions.some((p) => p.name === permission);
  }

  hasModulePermission(moduleId: string): boolean {
    const context = this.getCurrentContext();
    if (!context) return false;

    return context.permissions.modules[moduleId]?.enabled ?? false;
  }

  hasResourcePermission(resource: string, action: string, scope?: PermissionScope): boolean {
    const user = this.currentUser();
    if (!user) return false;

    return user.permissions.some((permission) => permission.resource === resource && permission.action === action && (!scope || permission.scope === scope));
  }
}

Integración con Module-Base

La librería security provee todos los servicios que module-base necesita:

// module-base consume estos servicios desde security
export class UserManagementModule implements BrowserModuleContract {
  constructor(
    private authService: AuthService, // ← Desde @libs-core/security
    private logSystem: LogSystem,
    private eventSystem: EventSystem
  ) {}

  async initialize(): Promise<void> {
    // Obtener contexto desde security
    const context = this.authService.getCurrentContext();

    if (context) {
      // Verificar permisos del módulo
      if (!this.authService.hasModulePermission('user-management')) {
        throw new Error('Insufficient permissions for user management');
      }

      this.logSystem.info('Module initialized for tenant', {
        tenantId: context.tenantId,
      });
    }
  }
}

Métricas de Éxito

Funcionalidad Core

Calidad y Performance

Compatibilidad y Seguridad

Configuración de Dependencias

package.json

{
  "name": "@libs-core/security",
  "dependencies": {
    "@libs-core/shared-models": "*",
    "@angular/core": "^20.0.0",
    "@angular/common": "^20.0.0",
    "@angular/router": "^20.0.0",
    "@ngrx/signals": "^18.0.0",
    "rxjs": "^7.8.0"
  },
  "peerDependencies": {
    "@libs-core/log-system": "*",
    "@libs-core/event-system": "*"
  }
}

Configuración de Imports

// src/lib/models/index.ts - Re-exports para conveniencia
export {
  // Tenant domain
  Context,
  Config,
  Permissions,
  Features,
  ContextInfo,

  // Auth domain
  UserProfile,
  AuthState,
  LoginCredentials,
  LoginResult,
  SelectionResult,

  // RBAC domain
  Role,
  Permission,
  PermissionScope,
  PermissionCondition,

  // UI domain
  BrandingConfig,
  ModuleConfig,
  UserPreferences,
} from '@libs-core/shared-models';

// Modelos específicos de security (si los hay)
export * from './security-config';

Comandos de Desarrollo

# Instalación después de shared-models
nx install

# Desarrollo y testing
nx serve security
nx test security --coverage
nx lint security

# Build y análisis
nx build security
nx graph --focus=security

# Verificar dependencias
nx run security:check-dependencies

# Testing de multitenancy
nx test security --grep="multitenancy"
nx test security --grep="context"

# Security audit
nx run security:security-audit

Próximos Pasos Inmediatos

Pre-requisito: Crear @libs-core/shared-models

  1. Crear librería shared-models según plan definido
  2. Implementar todos los modelos de tenant, auth, RBAC, UI y API
  3. Configurar exportaciones granulares para tree shaking
  4. Testing de tipos y validación de interfaces

Desarrollo de @libs-core/security

  1. Configurar dependencia de shared-models en security
  2. Migrar funcionalidad desde module-base (tokens, interceptors, servicios)
  3. Implementar AuthService usando tipos de shared-models
  4. Crear ContextInterceptor para headers automáticos
  5. Implementar guards multitenancy con Angular 20
  6. Testing comprehensivo de integración con shared-models y module-base

Orden de Ejecución

# 1. Crear shared-models primero
nx generate @nx/js:library shared-models --directory=libs/core

# 2. Desarrollar shared-models completamente
nx build shared-models
nx test shared-models

# 3. Actualizar security para usar shared-models
nx build security
nx test security

# 4. Migrar otras librerías a shared-models
nx run workspace:migrate-to-shared-models

Características Clave de la Arquitectura

Contexto de Tenant (Cliente)

Flujo de Autenticación Robusto

  1. Login Inicial: Autenticación básica + lista de tenants disponibles
  2. Selección de Tenant: Carga del contexto del usuario en el tenant
  3. Contexto Completo: Usuario + roles + permisos + configuración del tenant
  4. Headers Automáticos: X-Tenant-ID en todas las requests al servidor

Integración Seamless con Module-Base

// Module-base obtiene todo desde security - API simple
const context = this.authService.getCurrentContext();
const hasPermission = this.authService.hasModulePermission('user-management');
const canRead = this.authService.hasResourcePermission('users', 'read');

Escalabilidad y Performance