Autenticación API
La API de ClickAware utiliza JSON Web Tokens (JWT) para autenticar y autorizar todas las solicitudes. Este sistema garantiza la seguridad y permite el acceso granular a los recursos.
Flujo de Autenticación
Diagrama de Flujo
sequenceDiagram
participant Client
participant API
participant AuthService
participant Database
Client->>API: POST /auth/login
API->>AuthService: Validate credentials
AuthService->>Database: Check user data
Database-->>AuthService: User info + permissions
AuthService-->>API: JWT token + refresh token
API-->>Client: Authentication response
Note over Client,API: Subsequent requests
Client->>API: GET /users (with Bearer token)
API->>AuthService: Validate JWT
AuthService-->>API: Token valid + user permissions
API-->>Client: Protected resource
Endpoints de Autenticación
POST /auth/login
Autentica un usuario y devuelve tokens de acceso.
Request
POST /auth/login
Content-Type: application/json
{
"email": "usuario@empresa.com",
"password": "contraseña_segura"
}
Response Exitosa (200)
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600,
"user": {
"id": 12345,
"email": "usuario@empresa.com",
"name": "Juan Pérez",
"role": "USER",
"permissions": ["read:profile", "write:reports"],
"lastLogin": "2024-01-15T10:30:00Z"
}
},
"message": "Login successful",
"timestamp": "2024-01-15T10:30:00Z"
}
Errores Comunes
401 - Credenciales Inválidas
{
"success": false,
"error": {
"code": "INVALID_CREDENTIALS",
"message": "Email o contraseña incorrectos",
"details": "Las credenciales proporcionadas no coinciden con ningún usuario activo"
},
"timestamp": "2024-01-15T10:30:00Z"
}
429 - Demasiados Intentos
{
"success": false,
"error": {
"code": "TOO_MANY_ATTEMPTS",
"message": "Demasiados intentos de login",
"details": "Intenta nuevamente en 15 minutos",
"retryAfter": 900
},
"timestamp": "2024-01-15T10:30:00Z"
}
POST /auth/refresh
Renueva un token de acceso usando el refresh token.
Request
POST /auth/refresh
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response (200)
{
"success": true,
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 3600
},
"message": "Token refreshed successfully",
"timestamp": "2024-01-15T11:30:00Z"
}
POST /auth/logout
Invalida los tokens del usuario actual.
Request
POST /auth/logout
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json
{
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
Response (200)
{
"success": true,
"message": "Logout successful",
"timestamp": "2024-01-15T12:00:00Z"
}
Estructura del JWT
Header
{
"alg": "HS256",
"typ": "JWT",
"kid": "2024-key-01"
}
Payload
{
"sub": "12345",
"email": "usuario@empresa.com",
"name": "Juan Pérez",
"role": "USER",
"permissions": [
"read:profile",
"write:reports",
"read:campaigns"
],
"iat": 1642248600,
"exp": 1642252200,
"iss": "clickaware-api",
"aud": "clickaware-client"
}
Verificación
// Ejemplo de verificación en cliente JavaScript
function verifyToken(token) {
try {
const decoded = jwt.verify(token, publicKey, {
issuer: 'clickaware-api',
audience: 'clickaware-client'
});
return {
valid: true,
user: decoded,
expiresAt: new Date(decoded.exp * 1000)
};
} catch (error) {
return {
valid: false,
error: error.message
};
}
}
️ Niveles de Autorización
Roles de Usuario
| Rol | Descripción | Permisos Base |
|---|---|---|
| USER | Usuario final estándar | read:profile, write:reports |
| MANAGER | Supervisor de equipo | USER + read:team, manage:team |
| ADMIN | Administrador de sistema | MANAGER + admin:* |
| SUPER_ADMIN | Administrador global | Acceso completo |
Sistema de Permisos
Formato de Permisos
Los permisos siguen el formato: action:resource:scope
- action:
read,write,delete,admin - resource:
users,campaigns,reports,settings - scope:
own,team,organization,all
Ejemplos de Permisos
{
"permissions": [
"read:users:team", // Leer usuarios de su equipo
"write:campaigns:own", // Crear/editar sus campañas
"delete:reports:own", // Eliminar sus reportes
"admin:settings:organization", // Administrar configuración organizacional
"read:analytics:all" // Ver todas las analíticas
]
}
Middleware de Autorización
def require_permission(permission):
def decorator(func):
def wrapper(*args, **kwargs):
token = get_token_from_request()
user = validate_token(token)
if not user.has_permission(permission):
return unauthorized_response()
return func(*args, **kwargs)
return wrapper
return decorator
# Uso
@require_permission("read:users:team")
def get_team_users():
# Lógica del endpoint
pass
Gestión de Sesiones
Tokens de Acceso
- Duración: 1 hora por defecto
- Almacenamiento: Solo en memoria (nunca localStorage)
- Renovación: Automática usando refresh token
- Revocación: Inmediata en logout
Refresh Tokens
- Duración: 30 días
- Rotación: Nuevo token en cada refresh
- Almacenamiento: httpOnly cookies
- Familia: Invalidación en cadena por seguridad
Configuración de Cookies
Set-Cookie: refreshToken=eyJhbGciOiJIUzI1NiIs...;
HttpOnly;
Secure;
SameSite=Strict;
Max-Age=2592000;
Path=/auth
Seguridad Avanzada
Rate Limiting
Límites por Endpoint
| Endpoint | Límite | Ventana | Penalización |
|---|---|---|---|
/auth/login | 5 intentos | 15 min | Bloqueo temporal |
/auth/refresh | 10 intentos | 1 min | Invalidar refresh token |
/auth/forgot-password | 3 intentos | 1 hora | Alerta de seguridad |
Headers de Rate Limit
X-RateLimit-Limit: 5
X-RateLimit-Remaining: 3
X-RateLimit-Reset: 1642249200
X-RateLimit-Policy: 5;w=900;comment="Login attempts"
Detección de Anomalías
const SecurityMonitor = {
// Detectar patrones sospechosos
checkLoginPatterns: (userId, ipAddress, userAgent) => {
const recent = getRecentLogins(userId, 24 * 60 * 60);
// Múltiples IPs en corto tiempo
if (getUniqueIPs(recent).length > 5) {
return { risk: 'HIGH', reason: 'Multiple IPs' };
}
// Geolocalización inusual
if (isUnusualLocation(ipAddress, recent)) {
return { risk: 'MEDIUM', reason: 'Unusual location' };
}
// User agent diferente
if (isNewUserAgent(userAgent, recent)) {
return { risk: 'LOW', reason: 'New device' };
}
return { risk: 'NORMAL' };
}
};
2FA (Autenticación de Dos Factores)
Configuración 2FA
POST /auth/2fa/setup
POST /auth/2fa/setup
Authorization: Bearer {access_token}
Content-Type: application/json
{
"method": "totp", // or "sms", "email"
"phoneNumber": "+34612345678" // para SMS
}
Response:
{
"success": true,
"data": {
"qrCode": "data:image/png;base64,iVBORw0KGgoAAAANSUhE...",
"secret": "MFRGG43FMZQXIZLSMU4DKOBZGA4DMZTFMQ3GI5TFMU2GMOBZGU4DKYTF",
"backupCodes": [
"12345-67890",
"23456-78901",
"34567-89012"
]
}
}
Login con 2FA
POST /auth/login/2fa
{
"email": "usuario@empresa.com",
"password": "contraseña_segura",
"totpCode": "123456"
}
Implementación en Clientes
JavaScript/Node.js
class ClickAwareAuth {
constructor(baseURL) {
this.baseURL = baseURL;
this.accessToken = null;
this.refreshToken = null;
}
async login(email, password) {
try {
const response = await fetch(`${this.baseURL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
});
const data = await response.json();
if (data.success) {
this.accessToken = data.data.accessToken;
this.refreshToken = data.data.refreshToken;
// Auto-refresh antes de expirar
this.scheduleRefresh(data.data.expiresIn);
return data.data.user;
}
throw new Error(data.error.message);
} catch (error) {
console.error('Login failed:', error);
throw error;
}
}
async apiCall(endpoint, options = {}) {
const config = {
...options,
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json',
...options.headers
}
};
const response = await fetch(`${this.baseURL}${endpoint}`, config);
if (response.status === 401) {
// Token expirado, intentar refresh
await this.refreshAccessToken();
config.headers.Authorization = `Bearer ${this.accessToken}`;
return fetch(`${this.baseURL}${endpoint}`, config);
}
return response;
}
async refreshAccessToken() {
if (!this.refreshToken) {
throw new Error('No refresh token available');
}
const response = await fetch(`${this.baseURL}/auth/refresh`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ refreshToken: this.refreshToken })
});
const data = await response.json();
if (data.success) {
this.accessToken = data.data.accessToken;
this.refreshToken = data.data.refreshToken;
this.scheduleRefresh(data.data.expiresIn);
} else {
// Refresh token inválido, redirigir a login
this.logout();
throw new Error('Session expired');
}
}
scheduleRefresh(expiresIn) {
// Renovar 5 minutos antes de expirar
const refreshTime = (expiresIn - 300) * 1000;
setTimeout(() => this.refreshAccessToken(), refreshTime);
}
async logout() {
if (this.refreshToken) {
await fetch(`${this.baseURL}/auth/logout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${this.accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ refreshToken: this.refreshToken })
});
}
this.accessToken = null;
this.refreshToken = null;
}
}
// Uso
const auth = new ClickAwareAuth('https://api.clickaware.es/v1');
await auth.login('usuario@empresa.com', 'contraseña');
const response = await auth.apiCall('/users/profile');
Python
import requests
import jwt
import time
from typing import Optional, Dict, Any
class ClickAwareAuth:
def __init__(self, base_url: str):
self.base_url = base_url
self.access_token: Optional[str] = None
self.refresh_token: Optional[str] = None
def login(self, email: str, password: str) -> Dict[str, Any]:
"""Authenticate user and store tokens"""
response = requests.post(
f"{self.base_url}/auth/login",
json={"email": email, "password": password}
)
data = response.json()
if data.get("success"):
self.access_token = data["data"]["accessToken"]
self.refresh_token = data["data"]["refreshToken"]
return data["data"]["user"]
else:
raise Exception(data["error"]["message"])
def api_call(self, endpoint: str, method: str = "GET", **kwargs) -> requests.Response:
"""Make authenticated API call"""
headers = kwargs.get("headers", {})
headers["Authorization"] = f"Bearer {self.access_token}"
kwargs["headers"] = headers
response = requests.request(method, f"{self.base_url}{endpoint}", **kwargs)
if response.status_code == 401:
# Token expired, try refresh
self.refresh_access_token()
headers["Authorization"] = f"Bearer {self.access_token}"
response = requests.request(method, f"{self.base_url}{endpoint}", **kwargs)
return response
def refresh_access_token(self) -> None:
"""Refresh access token using refresh token"""
if not self.refresh_token:
raise Exception("No refresh token available")
response = requests.post(
f"{self.base_url}/auth/refresh",
json={"refreshToken": self.refresh_token}
)
data = response.json()
if data.get("success"):
self.access_token = data["data"]["accessToken"]
self.refresh_token = data["data"]["refreshToken"]
else:
self.logout()
raise Exception("Session expired")
def logout(self) -> None:
"""Logout and invalidate tokens"""
if self.refresh_token:
requests.post(
f"{self.base_url}/auth/logout",
headers={"Authorization": f"Bearer {self.access_token}"},
json={"refreshToken": self.refresh_token}
)
self.access_token = None
self.refresh_token = None
# Uso
auth = ClickAwareAuth("https://api.clickaware.es/v1")
user = auth.login("usuario@empresa.com", "contraseña")
response = auth.api_call("/users/profile")
Debugging y Troubleshooting
Logs de Autenticación
{
"timestamp": "2024-01-15T10:30:00Z",
"level": "INFO",
"event": "USER_LOGIN_SUCCESS",
"userId": 12345,
"email": "usuario@empresa.com",
"ipAddress": "192.168.1.100",
"userAgent": "Mozilla/5.0...",
"sessionId": "sess_abc123def456",
"duration": 250
}
Errores Comunes
Token Malformado
// Error: Token is malformed
// Solución: Verificar formato Bearer token
headers: {
"Authorization": "Bearer " + token, // Correcto
// NO: "Authorization": token
}
Token Expirado
// Error: Token has expired
// Solución: Implementar refresh automático
if (error.code === 'TOKEN_EXPIRED') {
await refreshToken();
// Reintentar petición original
}
Herramientas de Desarrollo
JWT Debugger
# Decodificar JWT (sin verificar firma)
echo "eyJhbGciOiJIUzI1NiIs..." | base64 -d
# Verificar token con CLI
jwt verify <token> --secret <secret> --issuer clickaware-api
Postman Collection
{
"info": { "name": "ClickAware Auth" },
"auth": {
"type": "bearer",
"bearer": [{ "key": "token", "value": "{{access_token}}" }]
},
"event": [
{
"listen": "prerequest",
"script": {
"exec": [
"// Auto-refresh token si está próximo a expirar",
"const token = pm.environment.get('access_token');",
"if (token && isTokenExpiringSoon(token)) {",
" pm.sendRequest({",
" url: pm.environment.get('base_url') + '/auth/refresh',",
" method: 'POST',",
" body: { refreshToken: pm.environment.get('refresh_token') }",
" }, (err, res) => {",
" if (!err && res.json().success) {",
" pm.environment.set('access_token', res.json().data.accessToken);",
" }",
" });",
"}"
]
}
}
]
}
Próximos Pasos
¿Ya dominas la autenticación? Continúa explorando:
- Gestión de Usuarios - CRUD de usuarios
- Guía de Usuario - Interfaz de usuario
- Panel de Admin - Administración