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
- Tipo: Librería Nx de Angular
- Ruta de importación:
@libs-core/security - Prioridad:
Alta- fundamental para la protección del sistema - Entorno: Cliente web con autenticación JWT, multitenancy nativo y protección CSP
- Nueva responsabilidad: Gestión completa de multitenancy (movido desde @libs-core/module-base)
- Dependencias:
@libs-core/shared-models(modelos y tipos compartidos)
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)
- Cliente Agnóstico de BD: El cliente no conoce detalles de base de datos
- Identificación por Tenant: Solo maneja
tenantIdpara identificar el contexto - Separación de Responsabilidades: Servidor gestiona BD, cliente gestiona UX
- Headers Automáticos: Envía
X-Tenant-IDen todas las requests - Contexto Reactivo: Estado de tenant reactivo con signals de Angular 20
Cambios Arquitectónicos Principales
- Multitenancy Cliente: Gestión del contexto de tenant en Angular
- Integración Module-Base: Provee servicios que module-base consume
- Configuración Directa: Sin tokens complejos, APIs simples
- Angular 20 Completo: Signals, standalone components, nueva sintaxis de control
- Separación Cliente-Servidor: Cliente maneja UX, servidor maneja datos
API Pública Planificada
Authentication & Multitenancy Layer
- AuthService: Autenticación JWT con contexto de tenant
- TenantContextService: Gestión del contexto actual del tenant
- TokenService: Gestión segura de tokens por tenant
- SessionService: Manejo de sesiones multitenancy
Authorization Layer (RBAC + Multitenancy)
- PermissionService: Verificación de permisos por tenant
- RoleService: Gestión de roles por tenant
- TenantPermissionService: Permisos específicos del tenant
- ModulePermissionService: Permisos para módulos por tenant
Multitenancy Core (Nueva Sección)
- ContextInterceptor: Interceptor para headers
X-Tenant-ID - ConfigService: Configuración específica por tenant
- ApiUrlService: URLs base por tenant
- ContextValidator: Validación de contexto de tenant
Guards y Resolvers (Angular 20 + Multitenancy)
- AuthGuard: Guard funcional standalone con contexto
- ContextGuard: Guard para validar contexto
- ModulePermissionGuard: Guard para permisos de módulos
- ContextResolver: Resolver de contexto
HTTP Security Layer (Multitenancy Ready)
- AuthInterceptor: Interceptor JWT con refresh automático
- ContextInterceptor: Inyección automática de
X-Tenant-ID - CSRFInterceptor: Protección CSRF
- SecurityHeadersInterceptor: Headers de seguridad
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:
- ✅ Consistencia: Mismos tipos en todo el workspace
- ✅ Mantenibilidad: Un solo lugar para cambios de modelo
- ✅ Tree Shaking: Solo se importan los tipos necesarios
- ✅ Type Safety: Validación estricta de tipos
- ✅ Documentación: JSDoc centralizada en 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
- Multitenancy Completo: Gestión nativa de contexto de tenant
- Integración Module-Base: Provee servicios requeridos por módulos
- Autenticación JWT: Con soporte completo de multitenancy
- Guards Angular 20: Funcionales standalone con tenant context
- Interceptores Avanzados: Headers automáticos de tenant
Calidad y Performance
- Coverage: ≥ 95% unit tests, ≥ 90% integration tests
- Bundle Size: < 50KB gzipped (incluyendo multitenancy completo)
- Type Safety: 100% tipado, sin uso de
any - Angular 20 Native: Signals, standalone components, nueva sintaxis
Compatibilidad y Seguridad
- Zero Trust: Validación en cada operación
- Tenant Isolation: Aislamiento completo entre tenants
- Browser Security: APIs modernas de seguridad
- Performance: Optimizado para multitenancy a escala
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
- Crear librería shared-models según plan definido
- Implementar todos los modelos de tenant, auth, RBAC, UI y API
- Configurar exportaciones granulares para tree shaking
- Testing de tipos y validación de interfaces
Desarrollo de @libs-core/security
- Configurar dependencia de shared-models en security
- Migrar funcionalidad desde module-base (tokens, interceptors, servicios)
- Implementar AuthService usando tipos de shared-models
- Crear ContextInterceptor para headers automáticos
- Implementar guards multitenancy con Angular 20
- 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)
- Identificación Única: Cada tenant se identifica por su
tenantId - Estado Reactivo: Contexto de tenant reactivo con signals de Angular
- Headers Automáticos:
X-Tenant-IDenviado automáticamente al servidor - Configuración por Tenant: UI y funcionalidades específicas por tenant
Flujo de Autenticación Robusto
- Login Inicial: Autenticación básica + lista de tenants disponibles
- Selección de Tenant: Carga del contexto del usuario en el tenant
- Contexto Completo: Usuario + roles + permisos + configuración del tenant
- Headers Automáticos:
X-Tenant-IDen 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
- Signals Reactivos: Estado de tenant reactivo en tiempo real
- Guards Funcionales: Validación de tenant y permisos eficiente
- Interceptores Optimizados: Headers automáticos sin overhead
- Cache Inteligente: Contexto de tenant cacheado de forma segura