Mejora de la autenticación y la gestión de tokens

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
This commit is contained in:
luis cespedes 2025-05-19 17:38:05 -04:00
parent 8a1434e553
commit f2ce7327d8
6 changed files with 300 additions and 189 deletions

View File

@ -40,6 +40,8 @@ function addToken(request: HttpRequest<unknown>, token: string): HttpRequest<unk
// Función para manejar errores 401 (token expirado)
function handle401Error(req: HttpRequest<unknown>, 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));

View File

@ -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<any>(null);
public user$ = this.userSubject.asObservable();
// Authentication state as a signal
public isAuthenticated = signal<boolean>(false);
constructor(
private http: HttpClient,
private router: Router,
private messageService: MessageService
) {}
async login(redirectUri?: string): Promise<void> {
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.reject('AuthService is deprecated. Use DirectAuthService instead.');
}
async logout(): Promise<void> {
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.resolve();
}
isLoggedIn(): Observable<boolean> {
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return from(Promise.resolve(false));
}
getToken(): Promise<string> {
console.warn('AuthService is deprecated. Use DirectAuthService instead.');
return Promise.resolve('');
}
async updateToken(): Promise<boolean> {
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;
}
}

View File

@ -32,16 +32,36 @@ export class DirectAuthService {
// Idle detection
private userActivity: any = null;
private userInactive = signal<boolean>(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
// 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<any> {
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<any> {
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<any>(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(() => {
// 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);
}

View File

@ -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
}
};

View File

@ -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
}
};

View File

@ -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,30 +556,42 @@ 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`
- 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"
![](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)
![](https://i.ibb.co/ycdTbg8h/imagen.png)
3. Completa los siguientes campos:
- Console Display Name: `LDAP`
@ -571,7 +602,8 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi
- Bind DN: `cn=admin,dc=correos,dc=com`
- Bind Credential: (la contraseña de admin LDAP)
![](https://i.ibb.co/nsFXWMJc/imagen.png)
![](https://i.ibb.co/nsFXWMJc/imagen.png)
- Edit Mode: `WRITABLE`
- Users DN: `ou=usuarios,dc=correos,dc=com`
- Username LDAP attribute: `uid`
@ -581,33 +613,39 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi
- 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)
![](https://i.ibb.co/JRq7tDB6/imagen.png)
7. Haz clic en "Sync all users"
### Configurar el Mapeo de Grupos LDAP
## 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)
![](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/ZRLDsqFs/imagen.png)
4. Haz clic en "Add mapper"
5. Completa los siguientes campos exactamente en este orden:
## 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,18 +665,20 @@ 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 proveedor LDAP, al guardar se regresará a la pantalla anterior, donde tendremos que hacer clic nuevamente en el mapper.
> al igual que con el provedor ldap , al guardar se regresara a la pantalla anterior , donde tendremos que hacer click nuevamente en el mapper
5. En la pantalla del mapper, haz clic en "Sync LDAP Groups to Keycloak"
7. 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
## Crear un Cliente para Angular
1. En el menú lateral izquierdo, selecciona "Clients"
@ -650,13 +690,14 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi
- Client ID: `angular-app`
- Name: `Angular Application`
- Haz clic en "Next"
![](https://i.ibb.co/XHCgj5Y/imagen.png)
![](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`
@ -664,9 +705,11 @@ Esto iniciará Keycloak con un usuario administrador "admin" y contraseña "admi
- 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.
![](https://i.ibb.co/Zp0r5wXY/imagen.png)
> **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.*