Skip to main content

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

{
"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

RolDescripciónPermisos Base
USERUsuario final estándarread:profile, write:reports
MANAGERSupervisor de equipoUSER + read:team, manage:team
ADMINAdministrador de sistemaMANAGER + admin:*
SUPER_ADMINAdministrador globalAcceso 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

EndpointLímiteVentanaPenalización
/auth/login5 intentos15 minBloqueo temporal
/auth/refresh10 intentos1 minInvalidar refresh token
/auth/forgot-password3 intentos1 horaAlerta 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: