Saltearse al contenido

Frontend Authentication

Frontend Authentication

Version: 1.0 Date: 2025-11-19 Project: Algesta Dashboard React

Tabla de Contenidos

  1. Descripción General de Autenticación
  2. Arquitectura de Autenticación
  3. Configuración de Autenticación
  4. Integración con Cliente API
  5. Componentes de Autenticación
  6. Esquemas de Validación
  7. Autorización (RBAC)
  8. Servicio de Almacenamiento
  9. Flujo de Estado de Autenticación
  10. Consideraciones de Seguridad
  11. Mejores Prácticas
  12. Pruebas de Autenticación

Descripción General de Autenticación

El Dashboard de Algesta implementa un sistema de autenticación seguro con las siguientes funcionalidades:

  • Autenticación basada en JWT con almacenamiento de tokens
  • localStorage para persistencia de tokens
  • Integración con react-query-auth para gestión de estado de autenticación
  • Cierre de sesión automático al expirar el token (respuestas 401)
  • Control de acceso basado en roles (RBAC) para autorización

Arquitectura de Autenticación

sequenceDiagram
    participant User
    participant LoginForm
    participant AuthLib
    participant APIClient
    participant APIGateway
    participant AuthMS
    participant LocalStorage

    User->>LoginForm: Enter credentials
    LoginForm->>AuthLib: useLogin(credentials)
    AuthLib->>APIClient: POST /api/auth/login
    APIClient->>APIGateway: Forward request
    APIGateway->>AuthMS: Authenticate user
    AuthMS-->>APIGateway: Return token + user
    APIGateway-->>APIClient: Return response
    APIClient-->>AuthLib: Return data
    AuthLib->>LocalStorage: Store token
    AuthLib->>LocalStorage: Store user
    AuthLib-->>LoginForm: Return user
    LoginForm->>User: Redirect to dashboard

Configuración de Autenticación

La configuración se define en src/lib/auth.tsx.

Configuración de Auth

import { configureAuth } from 'react-query-auth';
import { getUser, loginWithEmailAndPassword, registerWithEmailAndPassword, logout } from './api';
const authConfig = {
userFn: getUser, // Get current user
loginFn: loginWithEmailAndPassword, // Login function
registerFn: registerWithEmailAndPassword, // Register function
logoutFn: logout, // Logout function
};
export const { useUser, useLogin, useLogout, useRegister, AuthLoader } = configureAuth(authConfig);

Recuperación de Usuario

const getUser = async (): Promise<User | null> => {
try {
const userObj = getUserObj(); // Get from localStorage
if (userObj?.id) return userObj;
return null;
} catch (error) {
console.error('Error fetching user:', error);
return null;
}
};
export const getUserObj = (): User | null => {
const user = storageService.get<User | null>(StorageKeys.User);
if (!user?.id) return null;
return user;
};

Propósito: Recuperar usuario autenticado desde localStorage al inicializar la aplicación.

Gestión de Tokens

export const getToken = () => {
const token = storageService.get(StorageKeys.Token);
if (!token) return null;
return token;
};

Propósito: Recuperar token JWT para solicitudes API.

Flujo de Login

const loginWithEmailAndPassword = (data: LoginFormSchemaProps): Promise<AuthResponse> => {
return apiClient.post('/api/auth/login', data);
};
const loginFn = async (data: LoginFormSchemaProps) => {
const response = await loginWithEmailAndPassword(data);
const responseData = response.data.data;
const token = responseData?.token;
const user = responseData?.user;
if (user && token) {
storageService.set(StorageKeys.Token, token);
storageService.set(StorageKeys.User, user);
return user;
}
return null;
};

Pasos:

  1. Enviar credenciales a /api/auth/login
  2. Recibir token y datos de usuario
  3. Almacenar token en localStorage
  4. Almacenar datos de usuario en localStorage
  5. Retornar usuario para gestión de estado

Flujo de Logout

export const logout = (): Promise<void> => {
return new Promise(resolve => {
removeAuthStorageKeys();
resolve();
});
};
export const removeAuthStorageKeys = () => {
storageService.remove(StorageKeys.Token);
storageService.remove(StorageKeys.User);
};

Pasos:

  1. Eliminar token de localStorage
  2. Eliminar usuario de localStorage
  3. Redirigir a página de login (manejado por enrutamiento)

Flujo de Registro

const registerWithEmailAndPassword = (data: RegisterInput): Promise<AuthResponse> => {
return apiClient.post('/api/auth/register', data);
};
const registerFn = async (data: RegisterInput) => {
const response = await registerWithEmailAndPassword(data);
return response.data.data.user;
};

Pasos:

  1. Enviar datos de registro a /api/auth/register
  2. Recibir datos de usuario (el token puede estar incluido)
  3. Retornar usuario para gestión de estado
  4. Opcionalmente auto-login después del registro

Integración con Cliente API

Interceptor de Solicitud (Agregar Token JWT)

Ubicado en src/lib/api-client.ts:

const authRequestInterceptor = async (config: InternalAxiosRequestConfig) => {
const token = getToken();
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
};
API_CLIENT.interceptors.request.use(authRequestInterceptor);

Propósito: Agregar automáticamente el token JWT a todas las solicitudes API.

Interceptor de Respuesta (Manejar Errores 401)

const authResponseInterceptor = async (error: AxiosError) => {
if (error.response?.status === 401 && error.config?.url !== '/api/auth/login') {
// Token expired or invalid
SessionManager.clearSession();
toast.error('La sesión ha caducado, ingresa nuevamente');
return Promise.reject(new Error(error.message));
}
// Handle other errors
const message = error.response?.data?.message || 'An error occurred';
toast.error(message);
return Promise.reject(new Error(error.message));
};
API_CLIENT.interceptors.response.use(
response => response,
authResponseInterceptor
);

Propósito: Cerrar sesión automáticamente y redirigir en respuestas 401 (No autorizado).

Gestor de Sesión

export const SessionManager: SessionManagerProps = {
isRefreshing: false,
clearSession: () => {
// Remove all auth-related data
storageService.remove(StorageKeys.RefreshToken);
storageService.remove(StorageKeys.Token);
storageService.remove(StorageKeys.User);
storageService.remove(StorageKeys.DetailUserClient);
storageService.remove(StorageKeys.DetailUserProvider);
storageService.remove(StorageKeys.QuotationSelectProvider);
// Redirect to login
setTimeout(() => {
window.location.href = paths.auth.login.getHref();
}, 300);
},
};

Propósito: Limpiar todos los datos de sesión y redirigir al login al cerrar sesión o expirar la sesión.

Componentes de Autenticación

1. Formulario de Login

Ubicado en src/Funcionalidades/auth/Componentes/login-form.tsx:

Funcionalidades:

  • Campos de entrada para email y contraseña
  • Validación de formulario con Zod
  • Manejador de envío usando hook useLogin
  • Manejo y visualización de errores
  • Enlace a recuperación de contraseña
  • Enlaces a registro

Ejemplo:

import { useLogin } from '@/lib/auth';
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { loginSchema } from '../validations/login-schema';
const LoginForm = () => {
const { mutate: login, isPending } = useLogin();
const form = useForm({
resolver: zodResolver(loginSchema),
defaultValues: {
email: '',
password: '',
},
});
const onSubmit = form.handleSubmit((data) => {
login(data);
});
return (
<Form {...form}>
<form onSubmit={onSubmit}>
<InputTextFormField
name="email"
control={form.control}
label="Email"
type="email"
/>
<InputTextFormField
name="password"
control={form.control}
label="Password"
type="password"
/>
<Button type="submit" disabled={isPending}>
{isPending ? 'Logging in...' : 'Login'}
</Button>
</form>
</Form>
);
};

2. Signup Forms

Client Signup (signup-client-form.tsx):

  • Client-specific registration form
  • Additional fields: company name, address, etc.

Provider Signup (signup-provider-form.tsx):

  • Provider-specific registration form
  • Additional fields: services offered, certifications, etc.

Base Form (signup-base-form.tsx):

  • Shared form logic and validation
  • Common fields: email, password, name, etc.

3. Password Recovery

Forgot Password (forgot-password-form.tsx):

  • Email input to request password reset
  • Sends reset link to email

Reset Password (reset-password-form.tsx):

  • New password input
  • Confirm password validation
  • Token validation from URL

4. Auth Layout

Located in src/Funcionalidades/auth/Componentes/auth-layout.tsx:

Funcionalidades:

  • Consistent layout for all auth pages
  • Branding and logo
  • Responsive design
  • Background styling

5. Auth Loader

Propósito: Display loading state while checking authentication Estado.

Validation Schemas

Login Schema

Located in src/Funcionalidades/auth/validations/login-schema.ts:

import { z } from 'zod';
export const loginSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'),
});
export type LoginFormSchemaProps = z.infer<typeof loginSchema>;

Registration Schema

Located in src/utils/schemas/auth.ts:

import { z } from 'zod';
export const registerSchema = z.object({
email: z.string().email('Email inválido'),
password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'),
confirmPassword: z.string(),
name: z.string().min(2, 'El nombre debe tener al menos 2 caracteres'),
lastName: z.string().min(2, 'El apellido debe tener al menos 2 caracteres'),
phone: z.string().optional(),
// ... more fields
}).refine((data) => data.password === data.confirmPassword, {
message: 'Las contraseñas no coinciden',
path: ['confirmPassword'],
});
export type RegisterInput = z.infer<typeof registerSchema>;

Password Reset Schema

export const resetPasswordSchema = z.object({
password: z.string().min(6, 'La contraseña debe tener al menos 6 caracteres'),
confirmPassword: z.string(),
}).refine((data) => data.password === data.confirmPassword, {
message: 'Las contraseñas no coinciden',
path: ['confirmPassword'],
});

Authorization (RBAC)

User Roles

Defined in src/types/authorization.ts:

export const ROLES = {
ADMIN: 'admin',
AGENT: 'agent',
PROVIDER: 'provider',
CLIENT: 'client',
} as const;
export type RoleTypes = typeof ROLES[keyof typeof ROLES];

User Type

export interface User {
id: string;
email: string;
avatar?: string;
role?: RoleTypes;
name: string;
lastName: string;
phone?: string;
// ... more fields
}

Authorization Hook

Located in src/lib/authorization.tsx:

export const useAuthorization = () => {
const user = useUser();
if (!user.data) {
throw Error('User does not exist!');
}
const checkAccess = React.useCallback(
({ allowedRoles }: { allowedRoles: RoleTypes[] }) => {
if (allowedRoles && allowedRoles.length > 0 && user.data) {
return allowedRoles?.includes(user.data.role!);
}
return true;
},
[user.data]
);
return { checkAccess, role: user.data.role };
};

Authorization Componente

export const Authorization = ({
policyCheck,
allowedRoles,
forbiddenFallback = null,
children
}: AuthorizationProps) => {
const { checkAccess } = useAuthorization();
let canAccess = false;
if (allowedRoles) {
canAccess = checkAccess({ allowedRoles });
}
if (typeof policyCheck !== 'undefined') {
canAccess = policyCheck;
}
return <>{canAccess ? children : forbiddenFallback}</>;
};

Usage Example:

import { Authorization, ROLES } from '@/lib/authorization';
const AdminPanel = () => {
return (
<Authorization allowedRoles={[ROLES.ADMIN, ROLES.AGENT]}>
<AdminOnlyContent />
</Authorization>
);
};

Route-Level Authorization

See Frontend Routing & Navigation for route protection details.

Policies

Defined in src/lib/authorization.tsx:

src/lib/authorization.tsx
export const POLICIES = {
'comment:delete': (user: User) => {
if (user.role === ROLES.ADMIN || user.role === ROLES.AGENT) {
return true;
}
return false;
},
// ... more policies
};

Note: The examples above ('order:delete', 'user:create', etc.) are representative policy patterns. For the Completo and up-to-date list of policies actually implemented in the system, refer to src/lib/authorization.tsx. The current Implementación includes policies like 'comment:delete' and route-based policies in the ROUTE_POLICIES object.

Usage:

const { checkAccess } = useAuthorization();
const user = useUser();
const canDeleteOrder = POLICIES['order:delete'](user.data);
<Authorization policyCheck={canDeleteOrder}>
<DeleteOrderButton />
</Authorization>

Storage Service

Located in src/lib/storage.ts:

Propósito: Abstraction over localStorage with type safety.

Métodos

export const storageService = {
get<T>(key: string): T | null {
try {
const item = localStorage.getItem(key);
return item ? JSON.parse(item) : null;
} catch {
return null;
}
},
set<T>(key: string, value: T): void {
try {
localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error('Error saving to localStorage:', error);
}
},
remove(key: string): void {
try {
localStorage.removeItem(key);
} catch (error) {
console.error('Error removing from localStorage:', error);
}
},
clear(): void {
try {
localStorage.clear();
} catch (error) {
console.error('Error clearing localStorage:', error);
}
},
};

Storage Keys

Defined in src/config/constants.ts (authoritative source):

src/config/constants.ts
export const StorageKeys = {
Token: 'auth_token',
RefreshToken: 'refresh_token',
User: 'U534',
DetailUserClient: 'detail_client',
DetailUserProvider: 'detail_provider',
QuotationSelectProvider: 'quotation_select_provider',
} as const;

Note: These are the actual localStorage keys used by the application. The key names and Valors must match exactly what is defined in src/config/constants.ts to ensure proper storage and retrieval of authentication data.

Authentication State Flow

stateDiagram-v2
    [*] --> Unauthenticated
    Unauthenticated --> Authenticating: Login attempt
    Authenticating --> Authenticated: Success
    Authenticating --> Unauthenticated: Failure
    Authenticated --> Unauthenticated: Logout
    Authenticated --> Unauthenticated: Token expired (401)
    Authenticated --> Authenticated: Token valid

Security Considerations

1. Token Storage

Current Implementación:

  • JWT stored in localStorage (not cookies)
  • Vulnerable to XSS attacks if script injection occurs
  • Mitigated by Content Security Policy

Production Recommendations (not yet implemented):

The following items are recommendations for future enhancements and are not currently implemented in the codebase:

  • Consider httpOnly cookies for token storage
  • Implement refresh token rotation
  • Use secure and sameSite cookie flags

Note: Migration to httpOnly cookies would require coordinated changes on both frontend and backend (API Gateway and Auth Microservicio).

2. Token Expiration

  • Tokens expire after configured time (set by backend)
  • 401 responses trigger automatic logout
  • User redirected to login page
  • All auth data cleared from storage

3. HTTPS

  • Always use HTTPS in production
  • Prevents token interception (man-in-the-middle attacks)
  • SSL/TLS encryption for data in transit

4. CORS

  • API Gateway configured with CORS
  • Only allow trusted origins
  • Credentials included in requests

5. Password Security

Client-Side:

  • Passwords validated with Zod (minimum length, complexity)
  • Not stored in application state
  • Sent securely over HTTPS

Server-Side (Backend):

  • Passwords hashed with bcrypt
  • Salt rounds configured appropriately
  • Never stored in plain text

6. XSS Protection

  • React automatically escapes output
  • Content Security Policy headers
  • Sanitize user input

7. CSRF Protection

  • Stateless JWT authentication (no session cookies)
  • CSRF tokens not required for JWT-based auth
  • Use httpOnly cookies for additional CSRF protection

Best Practices

1. Token Management

  • Store token securely (consider httpOnly cookies)
  • Clear token on logout Completoly
  • Handle token expiration gracefully (automatic logout)
  • Consider refresh token Implementación for better UX

2. User Experience

  • Show loading states during authentication
  • Display clear error messages (invalid credentials, server errors)
  • Redirect to intended route after login (remember previous location)
  • Remember user preferences (theme, language)

3. Error Handling

  • Handle network errors (offline, timeout)
  • Handle invalid credentials (wrong email/password)
  • Handle token expiration (401 responses)
  • Provide user feedback (toast notifications, inline errors)

4. Authorization

  • Check roles on both client and server (never trust client)
  • Use Authorization Componente for UI-level control
  • Use route policies for navigation control
  • Implement fine-grained permissions with policies

5. Validation

  • Validate all inputs with Zod schemas
  • Provide clear validation messages
  • Validate on both client and server
  • Handle validation errors gracefully

6. Pruebas

  • Test authentication flows (login, logout, registration)
  • Test authorization checks (role-based access)
  • Test token expiration (401 handling)
  • Test error scenarios (network errors, invalid credentials)

Pruebas Authentication

Unit Pruebas

Example in src/Pruebas/Pruebas/login-form.test.tsx:

import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { LoginForm } from '@/features/auth/components/login-form';
describe('LoginForm', () => {
it('should render login form', () => {
render(<LoginForm />);
expect(screen.getByLabelText('Email')).toBeInTheDocument();
expect(screen.getByLabelText('Password')).toBeInTheDocument();
});
it('should show validation errors for invalid inputs', async () => {
render(<LoginForm />);
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(screen.getByText('Email inválido')).toBeInTheDocument();
});
});
it('should submit form with valid credentials', async () => {
const mockLogin = jest.fn();
render(<LoginForm />);
fireEvent.change(screen.getByLabelText('Email'), {
target: { value: 'test@example.com' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123' },
});
fireEvent.click(screen.getByRole('button', { name: /login/i }));
await waitFor(() => {
expect(mockLogin).toHaveBeenCalledWith({
email: 'test@example.com',
password: 'password123',
});
});
});
});

Integration Pruebas

Test Completo authentication flow:

describe('Authentication Flow', () => {
it('should login and redirect to dashboard', async () => {
// Test login → store token → redirect
});
it('should logout and redirect to login', async () => {
// Test logout → clear token → redirect
});
it('should handle 401 and auto-logout', async () => {
// Test 401 response → clear session → redirect
});
});

E2E Pruebas (Playwright)

test('login flow', async ({ page }) => {
await page.goto('/auth/login');
await page.fill('input[name="email"]', 'test@example.com');
await page.fill('input[name="password"]', 'password123');
await page.click('button[type="submit"]');
await page.waitForURL('/app/dashboard');
expect(page.url()).toContain('/app/dashboard');
});

References:

  • algesta-dashboard-react/src/lib/auth.tsx
  • algesta-dashboard-react/src/lib/authorization.tsx
  • algesta-dashboard-react/src/lib/api-client.ts
  • algesta-dashboard-react/src/lib/storage.ts
  • algesta-dashboard-react/src/Funcionalidades/auth/
  • algesta-dashboard-react/src/types/authorization.ts