From f2ce7327d897c57f07339eda59976ebdefe25524 Mon Sep 17 00:00:00 2001 From: luis cespedes Date: Mon, 19 May 2025 17:38:05 -0400 Subject: [PATCH] =?UTF-8?q?Mejora=20de=20la=20autenticaci=C3=B3n=20y=20la?= =?UTF-8?q?=20gesti=C3=B3n=20de=20tokens?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mejora la lógica de actualización de tokens teniendo en cuenta la actividad del usuario y las configuraciones del entorno. Elimina el obsoleto AuthService e integra el registro de actividad para una mejor supervisión. Actualiza las configuraciones del entorno para gestionar los tiempos de inactividad y el comportamiento de renovación de tokens. Aborda los posibles problemas de renovación de tokens relacionados con los usuarios inactivos y mejora la seguridad aplicando políticas estrictas de renovación de tokens. Traducción realizada con la versión gratuita del traductor DeepL.com --- src/app/interceptors/auth.interceptor.ts | 2 + src/app/services/auth.service.ts | 69 ------- src/app/services/direct-auth.service.ts | 144 +++++++++++-- src/environments/environment.prod.ts | 12 ++ src/environments/environment.ts | 12 ++ tutorial-keycloak-completo.md | 250 +++++++++++++---------- 6 files changed, 300 insertions(+), 189 deletions(-) delete mode 100644 src/app/services/auth.service.ts diff --git a/src/app/interceptors/auth.interceptor.ts b/src/app/interceptors/auth.interceptor.ts index 100d5ca..c60b956 100644 --- a/src/app/interceptors/auth.interceptor.ts +++ b/src/app/interceptors/auth.interceptor.ts @@ -40,6 +40,8 @@ function addToken(request: HttpRequest, token: string): HttpRequest, next: HttpHandlerFn, authService: DirectAuthService) { + // Intentar refrescar el token solo si el usuario está activo + // El método refreshToken() ya maneja el caso de inactividad internamente return authService.refreshToken().pipe( switchMap((token: any) => { return next(addToken(req, token.access_token)); diff --git a/src/app/services/auth.service.ts b/src/app/services/auth.service.ts deleted file mode 100644 index 36dc3d8..0000000 --- a/src/app/services/auth.service.ts +++ /dev/null @@ -1,69 +0,0 @@ -// This file is kept for reference only and is not used in the application. -// The application now uses DirectAuthService instead. - -import { Injectable, signal } from '@angular/core'; -import { HttpClient } from '@angular/common/http'; -import { BehaviorSubject, Observable, from } from 'rxjs'; -import { Router } from '@angular/router'; -import { MessageService } from 'primeng/api'; -import { environment } from '../../environments/environment'; - -@Injectable({ - providedIn: 'root' -}) -export class AuthService { - // User state - private userSubject = new BehaviorSubject(null); - public user$ = this.userSubject.asObservable(); - - // Authentication state as a signal - public isAuthenticated = signal(false); - - constructor( - private http: HttpClient, - private router: Router, - private messageService: MessageService - ) {} - - async login(redirectUri?: string): Promise { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return Promise.reject('AuthService is deprecated. Use DirectAuthService instead.'); - } - - async logout(): Promise { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return Promise.resolve(); - } - - isLoggedIn(): Observable { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return from(Promise.resolve(false)); - } - - getToken(): Promise { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return Promise.resolve(''); - } - - async updateToken(): Promise { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return false; - } - - getCurrentUser(): any { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return null; - } - - // Check if user has a specific role - hasRole(role: string): boolean { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return false; - } - - // Check if user has any of the specified roles - hasAnyRole(roles: string[]): boolean { - console.warn('AuthService is deprecated. Use DirectAuthService instead.'); - return false; - } -} \ No newline at end of file diff --git a/src/app/services/direct-auth.service.ts b/src/app/services/direct-auth.service.ts index 0e6b051..76d1e3d 100644 --- a/src/app/services/direct-auth.service.ts +++ b/src/app/services/direct-auth.service.ts @@ -32,16 +32,36 @@ export class DirectAuthService { // Idle detection private userActivity: any = null; private userInactive = signal(false); + private lastActivityTime: number = Date.now(); - // Percentage for inactivity timeout (90% of token lifetime) - private readonly INACTIVITY_PERCENTAGE = 0.9; + // Configuraciones de inactividad desde environment + private readonly INACTIVITY_PERCENTAGE: number; + private readonly MIN_INACTIVITY_TIME: number; + private readonly STRICT_TOKEN_RENEWAL: boolean; + private readonly ENABLE_ACTIVITY_LOGS: boolean; + private readonly LOG_INTERVAL: number; + + // Temporizador para logs de actividad + private activityLogTimer: any = null; constructor(private http: HttpClient) { + // Inicializar configuraciones desde environment + this.INACTIVITY_PERCENTAGE = environment.auth.inactivityPercentage; + this.MIN_INACTIVITY_TIME = environment.auth.minInactivityTime; + this.STRICT_TOKEN_RENEWAL = environment.auth.strictTokenRenewal; + this.ENABLE_ACTIVITY_LOGS = environment.auth.enableActivityLogs; + this.LOG_INTERVAL = environment.auth.logInterval; + // Intentar cargar el token del almacenamiento local al iniciar this.loadTokenFromStorage(); // Iniciar monitoreo de actividad this.setupActivityMonitoring(); + + // Iniciar logs de actividad si están habilitados + if (this.ENABLE_ACTIVITY_LOGS && this.LOG_INTERVAL > 0) { + this.startActivityLogging(); + } } // Cargar token del almacenamiento local @@ -53,25 +73,47 @@ export class DirectAuthService { // Verificar si el token ha expirado if (this.isTokenExpired()) { - // Si tiene refresh token, intentar renovar - if (this.tokenInfo.refresh_token) { + // Token expirado al cargar + // Si tiene refresh token y (no está en modo estricto o el usuario ha estado activo), intentar renovar + if (this.tokenInfo.refresh_token && (!this.STRICT_TOKEN_RENEWAL || this.isUserActive())) { + // Usuario activo o no en modo estricto, intentando renovar token this.refreshToken().subscribe(); } else { + // Usuario inactivo o sin refresh token, cerrando sesión this.logout(); } } else { // Decodificar info del usuario desde el token this.setUserFromToken(this.tokenInfo.access_token); // Configurar temporizador para renovación de token - this.startRefreshTokenTimer(); + // Solo si el usuario está activo (en modo estricto) o siempre (en modo no estricto) + if (!this.STRICT_TOKEN_RENEWAL || this.isUserActive()) { + this.startRefreshTokenTimer(); + } + + // Iniciar logs de actividad si están habilitados + if (this.ENABLE_ACTIVITY_LOGS && this.LOG_INTERVAL > 0) { + console.log('[Auth] Iniciando logs al cargar token existente'); + this.startActivityLogging(); + } } } catch (e) { - console.error('Error al cargar token:', e); + // Error al cargar token this.logout(); } } } + // Comprueba si el usuario ha estado activo dentro del período de inactividad + private isUserActive(): boolean { + const currentTime = Date.now(); + const inactiveTime = currentTime - this.lastActivityTime; + + // El usuario se considera activo si no ha estado inactivo por más tiempo que + // el porcentaje configurado del tiempo de vida del token o el tiempo mínimo + return inactiveTime < this.MIN_INACTIVITY_TIME; + } + // Login directo con credenciales public login(username: string, password: string): Observable { const headers = new HttpHeaders({ @@ -99,9 +141,18 @@ export class DirectAuthService { // Reiniciar detección de inactividad this.resetInactivity(); + + // Registrar tiempo de actividad + this.lastActivityTime = Date.now(); + + // Iniciar logs de actividad después del login si están habilitados + if (this.ENABLE_ACTIVITY_LOGS && this.LOG_INTERVAL > 0) { + console.log('[Auth] Iniciando logs después del login'); + this.startActivityLogging(); + } }), catchError(error => { - console.error('Error de autenticación:', error); + // Error de autenticación return throwError(() => new Error('Credenciales incorrectas o error de servidor')); }) ); @@ -115,6 +166,9 @@ export class DirectAuthService { // Detener monitoreo de actividad this.stopActivityMonitoring(); + // Detener logs de actividad + this.stopActivityLogging(); + // Limpiar datos de sesión localStorage.removeItem('keycloak_token'); this.tokenInfo = null; @@ -124,15 +178,43 @@ export class DirectAuthService { this.router.navigate(['/login']); } + // Iniciar logs de actividad en intervalos regulares + private startActivityLogging(): void { + // Detener cualquier temporizador existente + this.stopActivityLogging(); + + // Crear nuevo temporizador para logs + this.activityLogTimer = setInterval(() => { + const currentTime = Date.now(); + const inactiveTime = currentTime - this.lastActivityTime; + const inactiveSeconds = Math.round(inactiveTime / 1000); + + // Calcular tiempo hasta expiración del token + const tokenExpirationTime = this.getTokenExpirationTime(); + const expirationSeconds = Math.round(tokenExpirationTime / 1000); + + console.log(`[Auth] Inactividad: ${inactiveSeconds}s | Expiración token: ${expirationSeconds}s | Inactivo: ${this.userInactive()} | Umbral: ${Math.round(this.MIN_INACTIVITY_TIME/1000)}s`); + }, this.LOG_INTERVAL); + } + + // Detener logs de actividad + private stopActivityLogging(): void { + if (this.activityLogTimer) { + clearInterval(this.activityLogTimer); + this.activityLogTimer = null; + } + } + // Renovar token usando refresh token public refreshToken(): Observable { if (!this.tokenInfo?.refresh_token) { return throwError(() => new Error('No hay refresh token disponible')); } - // No refrescar token si el usuario está inactivo - if (this.userInactive()) { - console.log('Usuario inactivo, no se renovará el token'); + // Verificar si el usuario ha estado inactivo por más tiempo que el umbral configurado + // Si STRICT_TOKEN_RENEWAL está activado, verificar la actividad del usuario + if (this.STRICT_TOKEN_RENEWAL && !this.isUserActive()) { + // Usuario inactivo por más tiempo que el permitido, no se renovará el token this.logout(); return throwError(() => new Error('Usuario inactivo')); } @@ -146,9 +228,12 @@ export class DirectAuthService { .set('grant_type', 'refresh_token') .set('refresh_token', this.tokenInfo.refresh_token); + // Intentando renovar el token + return this.http.post(this.tokenEndpoint, params.toString(), { headers }) .pipe( tap(newTokenInfo => { + // Token renovado exitosamente // Actualizar información del token this.tokenInfo = newTokenInfo; localStorage.setItem('keycloak_token', JSON.stringify(newTokenInfo)); @@ -161,9 +246,15 @@ export class DirectAuthService { // Reiniciar detección de inactividad this.resetInactivity(); + + // Reiniciar logs si están habilitados + if (this.ENABLE_ACTIVITY_LOGS && this.LOG_INTERVAL > 0) { + console.log('[Auth] Reiniciando logs después de renovación de token'); + this.startActivityLogging(); + } }), catchError(error => { - console.error('Error al renovar token:', error); + // Error al renovar token // Si falla la renovación, forzar cierre de sesión this.logout(); return throwError(() => new Error('Error al renovar la sesión')); @@ -204,7 +295,7 @@ export class DirectAuthService { return currentTime >= expirationTime; } catch (e) { - console.error('Error al verificar expiración del token:', e); + // Error al verificar expiración del token return true; } } @@ -231,7 +322,7 @@ export class DirectAuthService { this.userInfo.set(user); } catch (e) { - console.error('Error al decodificar token:', e); + // Error al decodificar token this.userInfo.set(null); } } @@ -256,11 +347,19 @@ export class DirectAuthService { const timeToExpiry = expirationTime - currentTime; const refreshTime = timeToExpiry * 0.7; + // Calcular tiempos para la renovación del token + this.refreshTokenTimeout = setTimeout(() => { - this.refreshToken().subscribe(); + // Si STRICT_TOKEN_RENEWAL está activado, verificar la actividad del usuario + if (!this.STRICT_TOKEN_RENEWAL || this.isUserActive()) { + this.refreshToken().subscribe(); + } else { + // En modo estricto, si el usuario está inactivo, cerrar sesión + this.logout(); + } }, refreshTime); } catch (e) { - console.error('Error al configurar renovación de token:', e); + // Error al configurar renovación de token } } @@ -300,7 +399,7 @@ export class DirectAuthService { return Math.max(0, timeRemaining); } catch (error) { - console.error('Error al obtener tiempo de expiración del token:', error); + // Error al obtener tiempo de expiración del token return 0; } } @@ -333,8 +432,13 @@ export class DirectAuthService { // Reiniciar timer de inactividad basado en un porcentaje de la expiración del token private resetInactivity(): void { + // Registrar que el usuario está activo + this.lastActivityTime = Date.now(); + + // Marca el usuario como activo this.userInactive.set(false); + // Limpiar cualquier temporizador existente clearTimeout(this.userActivity); // Obtener el tiempo de expiración del token @@ -345,9 +449,15 @@ export class DirectAuthService { } // Calcular el tiempo de inactividad como el porcentaje configurado del tiempo de expiración - const inactivityTime = tokenExpirationTime * this.INACTIVITY_PERCENTAGE; + const inactivityTime = Math.min( + tokenExpirationTime * this.INACTIVITY_PERCENTAGE, + this.MIN_INACTIVITY_TIME + ); + + // Configurar timer de inactividad this.userActivity = setTimeout(() => { + // Umbral de inactividad alcanzado this.userInactive.set(true); }, inactivityTime); } diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 7947f62..34e1998 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -10,5 +10,17 @@ export const environment = { }, api: { baseUrl: '/api' + }, + auth: { + // Tiempo mínimo de inactividad en milisegundos (5 minutos para producción) + minInactivityTime: 5 * 60 * 1000, + // Porcentaje del tiempo de vida del token para considerar inactividad (90%) + inactivityPercentage: 0.9, + // Estricto: true = no renovar si inactivo, false = renovar siempre + strictTokenRenewal: true, + // Intervalo de log en milisegundos (desactivado en producción) + logInterval: 0, + // Deshabilitar logs de inactividad en producción + enableActivityLogs: false } }; \ No newline at end of file diff --git a/src/environments/environment.ts b/src/environments/environment.ts index 510c94f..b3ed113 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -10,5 +10,17 @@ export const environment = { }, api: { baseUrl: '/api' + }, + auth: { + // Tiempo mínimo de inactividad en milisegundos (30 segundos para desarrollo) + minInactivityTime: 30 * 1000, + // Porcentaje del tiempo de vida del token para considerar inactividad (90%) + inactivityPercentage: 0.9, + // Estricto: true = no renovar si inactivo, false = renovar siempre + strictTokenRenewal: true, + // Intervalo de log en milisegundos (1 segundo) + logInterval: 2000, + // Habilitar logs de inactividad + enableActivityLogs: true } }; \ No newline at end of file diff --git a/tutorial-keycloak-completo.md b/tutorial-keycloak-completo.md index 81e38ca..f65f0a4 100644 --- a/tutorial-keycloak-completo.md +++ b/tutorial-keycloak-completo.md @@ -468,39 +468,60 @@ sudo useradd -r -s /sbin/nologin keycloak # Asignar permisos sudo chown -R keycloak:keycloak /opt/keycloak ``` - -### Configurar usuario administrador inicial para Keycloak +## Configurar usuario administrador inicial para Keycloak Crea un archivo de propiedades: ```bash sudo nano /opt/keycloak/conf/keycloak.conf + ``` Agrega estas líneas: +> En mi caso esta máquina tiene la IP `192.168.1.27`, ajustar según sea necesario en una red real para la administración remota. + ``` # Configuración básica +hostname=192.168.1.27 http-port=8080 -https-port=8443 -hostname=localhost +http-enabled=true # Configuración de administrador inicial http-enabled=true -``` -![](https://i.ibb.co/GfHYrGtR/imagen.png) -### Iniciar Keycloak en modo desarrollo +``` + +![](https://i.ibb.co/S42YkQKg/image.png) + +## Crear un usuario administrador ```bash cd /opt/keycloak -export KEYCLOAK_ADMIN=admin -export KEYCLOAK_ADMIN_PASSWORD=admin -sudo -u keycloak bin/kc.sh start-dev +sudo -u keycloak /opt/keycloak/bin/kc.sh bootstrap-admin user --username admin + ``` +En este comando preguntará para crear la contraseña del usuario "admin" que usaremos como administrador. -### Configurar Keycloak como servicio (opcional) +## Iniciar Keycloak en modo desarrollo + +Sin hacer cd, corremos el siguiente comando: + +```bash +sudo -u keycloak bin/kc.sh start-dev + +``` + +## Iniciar Keycloak en modo productivo + +Similar al anterior, sin embargo se le quita la flag `-dev` únicamente: + +```bash +sudo -u keycloak bin/kc.sh start +``` + +## Configurar Keycloak como servicio productivo En otra terminal, crea un archivo de servicio systemd: @@ -519,9 +540,7 @@ After=network.target Type=idle User=keycloak Group=keycloak -Environment="KEYCLOAK_ADMIN=admin" -Environment="KEYCLOAK_ADMIN_PASSWORD=admin" -ExecStart=/opt/keycloak/bin/kc.sh start-dev +ExecStart=/opt/keycloak/bin/kc.sh start TimeoutStartSec=600 TimeoutStopSec=600 @@ -537,77 +556,96 @@ sudo systemctl enable keycloak sudo systemctl start keycloak ``` -## 5. Configuración de Keycloak en la interfaz web +Para ver el estado del servicio únicamente corre el comando: + +```bash +sudo systemctl status keycloak +``` + +> **Nota**: si quieres ver el log del servicio usa el comando `sudo journalctl -u keycloak -f` + +## Configuración de Keycloak en la interfaz web Ahora puedes acceder a la consola de administración de Keycloak en http://localhost:8080/admin/ e iniciar sesión con: -- Usuario: `admin` -- Contraseña: `admin` +- Usuario: `admin` +- Contraseña: la que configuraste anteriormente -Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admin" tambien pedira cambiar la contraseña -![](https://i.ibb.co/jk3LqsSN/imagen.png) +> No necesariamente es ese login siempre, todo depende de qué usuario crearon anteriormente y su contraseña. -### Crear un nuevo Reino (Realm) +![](https://i.ibb.co/dwrCJ3xr/image.png) + +## Crear un nuevo Reino (Realm) + +1. Haz clic en el menú desplegable superior izquierdo que dice "Manage realm" +2. Selecciona "Create Realm" +3. Ingresa el nombre: `angular-app` +4. Haz clic en "Create" -1. Haz clic en el menú desplegable superior izquierdo que dice "Manage realm" -2. Selecciona "Create Realm" -3. Ingresa el nombre: `angular-app` -4. Haz clic en "Create" ![](https://i.ibb.co/67kXBLFq/imagen.png) -### Configurar la Federación de Usuarios LDAP +## Configurar la Federación de Usuarios LDAP -1. En el menú lateral izquierdo, selecciona "User Federation" - -2. Haz clic en "Add Ldap provider" - ![enter image description here](https://i.ibb.co/ycdTbg8h/imagen.png) -3. Completa los siguientes campos: - - - Console Display Name: `LDAP` - - Vendor: `Other` - - Connection URL: `ldap://localhost:389` - - Enable StartTLS: `OFF` - - Bind Type: `simple` - - Bind DN: `cn=admin,dc=correos,dc=com` - - Bind Credential: (la contraseña de admin LDAP) +1. En el menú lateral izquierdo, selecciona "User Federation" + +2. Haz clic en "Add Ldap provider" + + ![](https://i.ibb.co/ycdTbg8h/imagen.png) + +3. Completa los siguientes campos: + + - Console Display Name: `LDAP` + - Vendor: `Other` + - Connection URL: `ldap://localhost:389` + - Enable StartTLS: `OFF` + - Bind Type: `simple` + - Bind DN: `cn=admin,dc=correos,dc=com` + - Bind Credential: (la contraseña de admin LDAP) + + ![](https://i.ibb.co/nsFXWMJc/imagen.png) + + - Edit Mode: `WRITABLE` + - Users DN: `ou=usuarios,dc=correos,dc=com` + - Username LDAP attribute: `uid` + - RDN LDAP attribute: `uid` + - UUID LDAP attribute: `entryUUID` + - User Object Classes: `inetOrgPerson, posixAccount` + - Custom User LDAP Filter: (dejar en blanco) + - Search Scope: `One Level` +4. Haz clic en "Test connection" y "Test authentication" para verificar + + ![](https://i.ibb.co/fzP0DQ03/imagen.png) + +5. Guarda la configuración + -![](https://i.ibb.co/nsFXWMJc/imagen.png) - - Edit Mode: `WRITABLE` - - Users DN: `ou=usuarios,dc=correos,dc=com` - - Username LDAP attribute: `uid` - - RDN LDAP attribute: `uid` - - UUID LDAP attribute: `entryUUID` - - User Object Classes: `inetOrgPerson, posixAccount` - - Custom User LDAP Filter: (dejar en blanco) - - Search Scope: `One Level` -4. Haz clic en "Test connection" y "Test authentication" para verificar - ![](https://i.ibb.co/fzP0DQ03/imagen.png) -5. Guarda la configuración - -> mas abajo hay mas configuraciones pero se quedan en su valor por defecto, -> despues de guardar regresara a la pestaña anterior donde tendremos que precionar en la configuracion recien creada +> Más abajo hay más configuraciones pero se quedan con su valor por defecto. Después de guardar regresará a la pestaña anterior donde tendremos que presionar en la configuración recién creada. ![](https://i.ibb.co/v438fVqL/imagen.png) - -6. En la pantalla del proveedor LDAP, ve a la pestaña "Synchronization" -![](https://i.ibb.co/JRq7tDB6/imagen.png) - -7. Haz clic en "Sync all users" - -### Configurar el Mapeo de Grupos LDAP +6. En la pantalla del proveedor LDAP, ve a la pestaña "Synchronization" -1. En la pantalla del proveedor LDAP, ve a la pestaña "Mappers" -![](https://i.ibb.co/Q7SqL2sM/imagen.png) -2. Haz clic en "Add mapper" -![enter image description here](https://i.ibb.co/ZRLDsqFs/imagen.png) -3.# Configuración Corregida del Mapeo de Grupos LDAP en Keycloak -3. En la pantalla del proveedor LDAP, ve a la pestaña "Mappers" + + ![](https://i.ibb.co/JRq7tDB6/imagen.png) -4. Haz clic en "Add mapper" +7. Haz clic en "Sync all users" -5. Completa los siguientes campos exactamente en este orden: + +## Configurar el Mapeo de Grupos LDAP + +1. En la pantalla del proveedor LDAP, ve a la pestaña "Mappers" + + ![](https://i.ibb.co/Q7SqL2sM/imagen.png) + +2. Haz clic en "Add mapper" + + ![](https://i.ibb.co/ZRLDsqFs/imagen.png) + + +## Configuración del Mapeo de Grupos LDAP en Keycloak + +3. Completa los siguientes campos exactamente en este orden: - **Name**: `group-mapper` - **Mapper type**: `group-ldap-mapper` @@ -618,7 +656,7 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi - **Preserve Group Inheritance**: `Off` _(¡IMPORTANTE! Debe estar desactivado para funcionar con Membership Attribute Type: UID)_ - **Ignore Missing Groups**: `Off` - **Membership LDAP Attribute**: `memberUid` - - **Membership Attribute Type**: `UID` _(¡IMPORTANTE! Debe ser UID, no DN )_ + - **Membership Attribute Type**: `UID` _(¡IMPORTANTE! Debe ser UID, no DN)_ - **Membership User LDAP Attribute**: `uid` - **LDAP Filter**: (dejar en blanco) - **Mode**: `LDAP_ONLY` @@ -627,46 +665,51 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi - **Mapped Group Attributes**: (dejar en blanco) - **Drop non-existing groups during sync**: `ON` - **Groups Path**: `/` -6. Haz clic en "Save" +4. Haz clic en "Save" + + +> **Nota importante**: Esta configuración está optimizada para tu estructura LDAP donde se utiliza `posixGroup` con atributos `memberUid` simples (no DNs). El punto crítico para evitar errores es asegurarte de que **"Preserve Group Inheritance" esté desactivado (Off)** cuando usas el tipo de membresía UID. -> **Nota importante**: Esta configuración está optimizada para tu estructura LDAP donde se utiliza `posixGroup` con atributos `memberUid` simples (no DNs). El punto crítico para evitar el error es asegurarte de que **"Preserve Group Inheritance" esté desactivado (Off)** cuando usas el tipo de membresía UID. ![](https://i.ibb.co/bjVYx4Zn/imagen.png) - - -> al igual que con el provedor ldap , al guardar se regresara a la pantalla anterior , donde tendremos que hacer click nuevamente en el mapper -7. En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak" +> Al igual que con el proveedor LDAP, al guardar se regresará a la pantalla anterior, donde tendremos que hacer clic nuevamente en el mapper. + +5. En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak" + ![](https://i.ibb.co/SD1GFcDZ/imagen.png) + +## Crear un Cliente para Angular + +1. En el menú lateral izquierdo, selecciona "Clients" + +2. Haz clic en "Create client" + +3. Completa: + + - Client type: `OpenID Connect` + - Client ID: `angular-app` + - Name: `Angular Application` + - Haz clic en "Next" + + ![](https://i.ibb.co/XHCgj5Y/imagen.png) + +4. En la siguiente pantalla (para aplicaciones SPA modernas): + + - Client authentication: `OFF` (para aplicaciones SPA) + - Authorization: `OFF` + - Haz clic en "Next" +5. En la siguiente pantalla: + + - Root URL: `http://localhost:4200` + - Home URL: `/` + - Valid redirect URIs: `http://localhost:4200/*` + - Web origins: `http://localhost:4200` (o usar `+` para permitir todos los orígenes durante desarrollo) + - Haz clic en "Save" + + ![](https://i.ibb.co/Zp0r5wXY/imagen.png) -### Crear un Cliente para Angular -1. En el menú lateral izquierdo, selecciona "Clients" - -2. Haz clic en "Create client" - -3. Completa: - - - Client type: `OpenID Connect` - - Client ID: `angular-app` - - Name: `Angular Application` - - Haz clic en "Next" -![](https://i.ibb.co/XHCgj5Y/imagen.png) -4. En la siguiente pantalla (para aplicaciones SPA modernas): - - - Client authentication: `OFF` (para aplicaciones SPA) - - Authorization: `OFF` - - Haz clic en "Next" - - -5. En la siguiente pantalla: - - - Root URL: `http://localhost:4200` - - Home URL: `/` - - Valid redirect URIs: `http://localhost:4200/*` - - Web origins: `http://localhost:4200` (o usar `+` para permitir todos los orígenes durante desarrollo) - - Haz clic en "Save" -![](https://i.ibb.co/Zp0r5wXY/imagen.png) - -Para aplicaciones que no son SPA, puedes habilitar "Client authentication" y obtener un client secret que deberás usar en la configuración. +> **Nota**: Para aplicaciones que no son SPA, puedes habilitar "Client authentication" y obtener un client secret que deberás usar en la configuración. ## 6. Integración con Angular: Enfoque básico @@ -1761,3 +1804,4 @@ Con la configuración y conocimientos adquiridos en este tutorial, estarás bien --- *Nota final: Recuerda que la seguridad es un proceso continuo, no un estado. Mantén todos los componentes actualizados y revisa regularmente las configuraciones y políticas de seguridad para adaptarte a nuevas amenazas y requisitos.* +