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
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<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(() => {
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);
}

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,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.*