specleap-framework 2.1.0 → 2.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/backend.md +3 -3
- package/.agents/frontend.md +2 -2
- package/.agents/producto.md +9 -11
- package/.claude/hooks/spec-guard.sh +132 -0
- package/.claude/settings.json.template +15 -0
- package/.clinerules +3 -3
- package/.coderabbit.yaml +2 -4
- package/.commands/compliance.md +89 -0
- package/.commands/inicio.md +15 -15
- package/.commands/nuevo/README.md +2 -2
- package/.commands/planificar.md +1 -1
- package/.continue/rules/04-git-workflow.md +5 -5
- package/.continuerules +3 -4
- package/.cursorrules +1 -1
- package/.github/copilot-instructions.md +1 -1
- package/.specleap/i18n/en.json +177 -0
- package/.specleap/i18n/es.json +177 -0
- package/.specleap/i18n.sh +63 -0
- package/CHANGELOG.md +276 -0
- package/CLAUDE.md +54 -13
- package/README.md +169 -528
- package/SETUP.md +16 -13
- package/openspec/INDEX.md +53 -0
- package/openspec/README.md +104 -0
- package/openspec/SPEC-FORMAT.md +168 -0
- package/openspec/changes/.gitkeep +0 -0
- package/openspec/cli/COMMAND_REFERENCE.md +817 -0
- package/openspec/cli/README.md +189 -0
- package/openspec/cli/apply.sh +229 -0
- package/openspec/cli/archive.sh +240 -0
- package/openspec/cli/code-review.sh +207 -0
- package/openspec/cli/common.sh +171 -0
- package/openspec/cli/enrich.sh +188 -0
- package/openspec/cli/ff.sh +329 -0
- package/openspec/cli/new.sh +260 -0
- package/openspec/cli/openspec +82 -0
- package/openspec/cli/report.sh +244 -0
- package/openspec/cli/status.sh +178 -0
- package/openspec/cli/verify.sh +246 -0
- package/openspec/config.yaml +76 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/00-original-user-story.md +5 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/01-refined-user-story.md +106 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/README.md +333 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/design.md +461 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/proposal.md +124 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/specs/functional/F001-authentication.spec.md +399 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/specs/technical/T001-jwt-implementation.spec.md +606 -0
- package/openspec/examples/CHANGE-SAMPLE-001-user-authentication/tasks.md +433 -0
- package/openspec/examples/MERMAID_DIAGRAMS.md +481 -0
- package/openspec/examples/README.md +334 -0
- package/openspec/specs/functional/.gitkeep +0 -0
- package/openspec/specs/integration/.gitkeep +0 -0
- package/openspec/specs/security/.gitkeep +0 -0
- package/openspec/specs/technical/.gitkeep +0 -0
- package/openspec/templates/.coderabbit.yaml +259 -0
- package/openspec/templates/design.md +181 -0
- package/openspec/templates/proposal.md +79 -0
- package/openspec/templates/tasks.md +193 -0
- package/package.json +10 -5
- package/rules/git-workflow.md +3 -3
- package/rules/session-protocol.md +3 -3
- package/scripts/README.md +13 -25
- package/scripts/compliance-audit.sh +325 -0
- package/scripts/generate-contract.sh +4 -4
- package/scripts/install-skills.sh +12 -11
- package/scripts/lib/render-contrato.py +1 -1
- package/scripts/quality-baseline.sh +210 -0
- package/scripts/quality-healing.sh +241 -0
- package/setup.sh +3 -3
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/core.cpython-314.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/design_system.cpython-314.pyc +0 -0
- package/.claude/skills/ui-ux-pro-max/scripts/__pycache__/search.cpython-314.pyc +0 -0
- package/scripts/lib/jira-project-utils.sh +0 -222
- package/scripts/setup-mcp.sh +0 -654
- package/scripts/test-cuestionario.sh +0 -428
|
@@ -0,0 +1,606 @@
|
|
|
1
|
+
# T001 — Implementación JWT
|
|
2
|
+
|
|
3
|
+
| Campo | Valor |
|
|
4
|
+
|-------|-------|
|
|
5
|
+
| ID | T001 |
|
|
6
|
+
| Título | Implementación de Autenticación JWT |
|
|
7
|
+
| Dominio | technical |
|
|
8
|
+
| Estado | implemented |
|
|
9
|
+
| Autor | SpecLeap Contributor |
|
|
10
|
+
| Fecha Creación | 2026-02-12 |
|
|
11
|
+
| Última Actualización | 2026-02-12 |
|
|
12
|
+
| Versión | 1.0 |
|
|
13
|
+
| Relacionada | F001-authentication |
|
|
14
|
+
|
|
15
|
+
## Descripción
|
|
16
|
+
|
|
17
|
+
Especificación técnica de la implementación de autenticación basada en JSON Web Tokens (JWT) con algoritmo RS256, gestión de claves RSA, y middleware de autorización.
|
|
18
|
+
|
|
19
|
+
## Arquitectura de Componentes
|
|
20
|
+
|
|
21
|
+
### Componentes Implementados
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
AuthController (API Layer)
|
|
25
|
+
├── AuthService (Business Logic)
|
|
26
|
+
│ ├── UserRepository (Data Access)
|
|
27
|
+
│ └── JWTService (Token Management)
|
|
28
|
+
│ └── RSA Keys (public/private)
|
|
29
|
+
│
|
|
30
|
+
├── RateLimitMiddleware
|
|
31
|
+
│ └── LoginAttemptsRepository
|
|
32
|
+
│
|
|
33
|
+
└── AuthMiddleware (Request Guard)
|
|
34
|
+
└── JWTService (validation)
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Especificación de Clases
|
|
38
|
+
|
|
39
|
+
### JWTService
|
|
40
|
+
|
|
41
|
+
**Responsabilidad:** Generación y validación de tokens JWT.
|
|
42
|
+
|
|
43
|
+
#### Métodos
|
|
44
|
+
|
|
45
|
+
```php
|
|
46
|
+
class JWTService
|
|
47
|
+
{
|
|
48
|
+
/**
|
|
49
|
+
* Genera un token JWT para un usuario.
|
|
50
|
+
*
|
|
51
|
+
* @param User $user Usuario autenticado
|
|
52
|
+
* @param bool $remember Extender expiración a 30 días
|
|
53
|
+
* @return string Token JWT firmado
|
|
54
|
+
*/
|
|
55
|
+
public function generateToken(User $user, bool $remember = false): string;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Valida un token JWT.
|
|
59
|
+
*
|
|
60
|
+
* @param string $token Token a validar
|
|
61
|
+
* @return array Payload decodificado
|
|
62
|
+
* @throws TokenExpiredException Si el token expiró
|
|
63
|
+
* @throws TokenInvalidException Si la firma no es válida
|
|
64
|
+
*/
|
|
65
|
+
public function validateToken(string $token): array;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Refresca un token JWT.
|
|
69
|
+
*
|
|
70
|
+
* @param string $token Token actual
|
|
71
|
+
* @return string Nuevo token
|
|
72
|
+
*/
|
|
73
|
+
public function refreshToken(string $token): string;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Revoca un token (añade a blacklist).
|
|
77
|
+
*
|
|
78
|
+
* @param string $token Token a revocar
|
|
79
|
+
* @return void
|
|
80
|
+
*/
|
|
81
|
+
public function revokeToken(string $token): void;
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
#### Estructura del Token JWT
|
|
86
|
+
|
|
87
|
+
**Header:**
|
|
88
|
+
```json
|
|
89
|
+
{
|
|
90
|
+
"alg": "RS256",
|
|
91
|
+
"typ": "JWT"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**Payload:**
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"sub": 123, // user_id
|
|
99
|
+
"email": "user@example.com",
|
|
100
|
+
"iat": 1707753600, // issued at (Unix timestamp)
|
|
101
|
+
"exp": 1707840000, // expiration (Unix timestamp)
|
|
102
|
+
"remember": false // opcional
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Signature:**
|
|
107
|
+
```
|
|
108
|
+
RSASHA256(
|
|
109
|
+
base64UrlEncode(header) + "." +
|
|
110
|
+
base64UrlEncode(payload),
|
|
111
|
+
privateKey
|
|
112
|
+
)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
#### Configuración de Claves RSA
|
|
116
|
+
|
|
117
|
+
**Generación:**
|
|
118
|
+
```bash
|
|
119
|
+
# Private key (4096 bits)
|
|
120
|
+
openssl genrsa -out private.pem 4096
|
|
121
|
+
|
|
122
|
+
# Public key
|
|
123
|
+
openssl rsa -in private.pem -pubout -out public.pem
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
**Ubicación:**
|
|
127
|
+
```
|
|
128
|
+
storage/keys/
|
|
129
|
+
├── private.pem (chmod 600, no commitear)
|
|
130
|
+
└── public.pem (chmod 644, OK commitear)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
**Variables de entorno:**
|
|
134
|
+
```env
|
|
135
|
+
JWT_PRIVATE_KEY_PATH=/path/to/private.pem
|
|
136
|
+
JWT_PUBLIC_KEY_PATH=/path/to/public.pem
|
|
137
|
+
JWT_TTL=86400 # 24 horas en segundos
|
|
138
|
+
JWT_TTL_REMEMBER=2592000 # 30 días en segundos
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### AuthService
|
|
144
|
+
|
|
145
|
+
**Responsabilidad:** Lógica de negocio de autenticación.
|
|
146
|
+
|
|
147
|
+
#### Métodos
|
|
148
|
+
|
|
149
|
+
```php
|
|
150
|
+
class AuthService
|
|
151
|
+
{
|
|
152
|
+
/**
|
|
153
|
+
* Autentica un usuario con email y contraseña.
|
|
154
|
+
*
|
|
155
|
+
* @param string $email
|
|
156
|
+
* @param string $password
|
|
157
|
+
* @param bool $remember
|
|
158
|
+
* @return array ['token' => string, 'user' => User]
|
|
159
|
+
* @throws AuthenticationException Si credenciales inválidas
|
|
160
|
+
*/
|
|
161
|
+
public function authenticate(
|
|
162
|
+
string $email,
|
|
163
|
+
string $password,
|
|
164
|
+
bool $remember = false
|
|
165
|
+
): array;
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Cierra la sesión de un usuario.
|
|
169
|
+
*
|
|
170
|
+
* @param string $token Token a invalidar
|
|
171
|
+
* @return void
|
|
172
|
+
*/
|
|
173
|
+
public function logout(string $token): void;
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
#### Flujo de Autenticación
|
|
178
|
+
|
|
179
|
+
1. **Validar inputs:**
|
|
180
|
+
- Email: formato válido, max 255 chars
|
|
181
|
+
- Password: min 8 chars, max 255 chars
|
|
182
|
+
|
|
183
|
+
2. **Buscar usuario:**
|
|
184
|
+
```php
|
|
185
|
+
$user = UserRepository::findByEmail($email);
|
|
186
|
+
if (!$user) {
|
|
187
|
+
throw new AuthenticationException('Credenciales inválidas');
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
3. **Verificar contraseña:**
|
|
192
|
+
```php
|
|
193
|
+
if (!Hash::check($password, $user->password_hash)) {
|
|
194
|
+
// Registrar intento fallido
|
|
195
|
+
LoginAttempt::create([
|
|
196
|
+
'email' => $email,
|
|
197
|
+
'ip_address' => request()->ip(),
|
|
198
|
+
'success' => false
|
|
199
|
+
]);
|
|
200
|
+
throw new AuthenticationException('Credenciales inválidas');
|
|
201
|
+
}
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
4. **Generar token:**
|
|
205
|
+
```php
|
|
206
|
+
$token = JWTService::generateToken($user, $remember);
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
5. **Registrar intento exitoso:**
|
|
210
|
+
```php
|
|
211
|
+
LoginAttempt::create([
|
|
212
|
+
'email' => $email,
|
|
213
|
+
'ip_address' => request()->ip(),
|
|
214
|
+
'success' => true
|
|
215
|
+
]);
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
6. **Retornar respuesta:**
|
|
219
|
+
```php
|
|
220
|
+
return [
|
|
221
|
+
'token' => $token,
|
|
222
|
+
'token_type' => 'Bearer',
|
|
223
|
+
'expires_in' => $remember ? 2592000 : 86400,
|
|
224
|
+
'user' => [
|
|
225
|
+
'id' => $user->id,
|
|
226
|
+
'email' => $user->email
|
|
227
|
+
]
|
|
228
|
+
];
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
### AuthMiddleware
|
|
234
|
+
|
|
235
|
+
**Responsabilidad:** Proteger rutas que requieren autenticación.
|
|
236
|
+
|
|
237
|
+
#### Implementación
|
|
238
|
+
|
|
239
|
+
```php
|
|
240
|
+
class AuthMiddleware
|
|
241
|
+
{
|
|
242
|
+
public function handle(Request $request, Closure $next)
|
|
243
|
+
{
|
|
244
|
+
// 1. Extraer token del header
|
|
245
|
+
$token = $request->bearerToken();
|
|
246
|
+
|
|
247
|
+
if (!$token) {
|
|
248
|
+
return response()->json([
|
|
249
|
+
'error' => 'No autenticado. Token requerido.'
|
|
250
|
+
], 401);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
// 2. Validar token
|
|
255
|
+
$payload = JWTService::validateToken($token);
|
|
256
|
+
|
|
257
|
+
// 3. Obtener usuario
|
|
258
|
+
$user = User::find($payload['sub']);
|
|
259
|
+
|
|
260
|
+
if (!$user) {
|
|
261
|
+
return response()->json([
|
|
262
|
+
'error' => 'Usuario no encontrado'
|
|
263
|
+
], 401);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// 4. Adjuntar usuario al request
|
|
267
|
+
$request->setUserResolver(function () use ($user) {
|
|
268
|
+
return $user;
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
return $next($request);
|
|
272
|
+
|
|
273
|
+
} catch (TokenExpiredException $e) {
|
|
274
|
+
return response()->json([
|
|
275
|
+
'error' => 'Token expirado. Inicie sesión nuevamente.'
|
|
276
|
+
], 401);
|
|
277
|
+
|
|
278
|
+
} catch (TokenInvalidException $e) {
|
|
279
|
+
return response()->json([
|
|
280
|
+
'error' => 'Token inválido'
|
|
281
|
+
], 401);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### RateLimitMiddleware
|
|
290
|
+
|
|
291
|
+
**Responsabilidad:** Protección contra brute force.
|
|
292
|
+
|
|
293
|
+
#### Configuración
|
|
294
|
+
|
|
295
|
+
```php
|
|
296
|
+
const MAX_ATTEMPTS = 5;
|
|
297
|
+
const LOCKOUT_TIME = 1800; // 30 minutos en segundos
|
|
298
|
+
const DECAY_MINUTES = 15;
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
#### Implementación
|
|
302
|
+
|
|
303
|
+
```php
|
|
304
|
+
class RateLimitMiddleware
|
|
305
|
+
{
|
|
306
|
+
public function handle(Request $request, Closure $next)
|
|
307
|
+
{
|
|
308
|
+
$email = $request->input('email');
|
|
309
|
+
$ip = $request->ip();
|
|
310
|
+
$key = "login_attempts:{$email}:{$ip}";
|
|
311
|
+
|
|
312
|
+
// 1. Contar intentos fallidos recientes
|
|
313
|
+
$attempts = LoginAttempt::where('email', $email)
|
|
314
|
+
->where('ip_address', $ip)
|
|
315
|
+
->where('success', false)
|
|
316
|
+
->where('attempted_at', '>=', now()->subMinutes(self::DECAY_MINUTES))
|
|
317
|
+
->count();
|
|
318
|
+
|
|
319
|
+
// 2. Verificar si está bloqueado
|
|
320
|
+
if ($attempts >= self::MAX_ATTEMPTS) {
|
|
321
|
+
$lastAttempt = LoginAttempt::where('email', $email)
|
|
322
|
+
->where('ip_address', $ip)
|
|
323
|
+
->orderBy('attempted_at', 'desc')
|
|
324
|
+
->first();
|
|
325
|
+
|
|
326
|
+
$lockoutUntil = $lastAttempt->attempted_at
|
|
327
|
+
->addSeconds(self::LOCKOUT_TIME);
|
|
328
|
+
|
|
329
|
+
if (now()->lt($lockoutUntil)) {
|
|
330
|
+
$remainingMinutes = now()->diffInMinutes($lockoutUntil);
|
|
331
|
+
|
|
332
|
+
return response()->json([
|
|
333
|
+
'error' => "Cuenta temporalmente bloqueada por intentos fallidos. Intente nuevamente en {$remainingMinutes} minutos."
|
|
334
|
+
], 429);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
return $next($request);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
---
|
|
344
|
+
|
|
345
|
+
## Modelo de Datos
|
|
346
|
+
|
|
347
|
+
### Tabla: `login_attempts`
|
|
348
|
+
|
|
349
|
+
```sql
|
|
350
|
+
CREATE TABLE login_attempts (
|
|
351
|
+
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
|
352
|
+
email VARCHAR(255) NOT NULL,
|
|
353
|
+
ip_address VARCHAR(45) NOT NULL,
|
|
354
|
+
user_agent TEXT,
|
|
355
|
+
success BOOLEAN DEFAULT FALSE,
|
|
356
|
+
attempted_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
357
|
+
|
|
358
|
+
INDEX idx_email_ip (email, ip_address),
|
|
359
|
+
INDEX idx_attempted_at (attempted_at)
|
|
360
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
**Campos:**
|
|
364
|
+
- `email`: Email del intento (puede no existir en users)
|
|
365
|
+
- `ip_address`: IPv4 o IPv6
|
|
366
|
+
- `user_agent`: User agent del navegador
|
|
367
|
+
- `success`: true = login exitoso, false = fallido
|
|
368
|
+
- `attempted_at`: Timestamp del intento
|
|
369
|
+
|
|
370
|
+
**Índices:**
|
|
371
|
+
- `idx_email_ip`: Para rate limiting (WHERE email AND ip_address)
|
|
372
|
+
- `idx_attempted_at`: Para cleanup de registros antiguos
|
|
373
|
+
|
|
374
|
+
---
|
|
375
|
+
|
|
376
|
+
## Endpoints API
|
|
377
|
+
|
|
378
|
+
### POST /api/v1/auth/login
|
|
379
|
+
|
|
380
|
+
**Middleware:** RateLimitMiddleware
|
|
381
|
+
|
|
382
|
+
**Request:**
|
|
383
|
+
```http
|
|
384
|
+
POST /api/v1/auth/login HTTP/1.1
|
|
385
|
+
Content-Type: application/json
|
|
386
|
+
|
|
387
|
+
{
|
|
388
|
+
"email": "user@example.com",
|
|
389
|
+
"password": "SecureP@ssw0rd",
|
|
390
|
+
"remember": false
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
**Response (200 OK):**
|
|
395
|
+
```json
|
|
396
|
+
{
|
|
397
|
+
"token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
|
|
398
|
+
"token_type": "Bearer",
|
|
399
|
+
"expires_in": 86400,
|
|
400
|
+
"user": {
|
|
401
|
+
"id": 123,
|
|
402
|
+
"email": "user@example.com"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
---
|
|
408
|
+
|
|
409
|
+
### POST /api/v1/auth/logout
|
|
410
|
+
|
|
411
|
+
**Middleware:** AuthMiddleware
|
|
412
|
+
|
|
413
|
+
**Request:**
|
|
414
|
+
```http
|
|
415
|
+
POST /api/v1/auth/logout HTTP/1.1
|
|
416
|
+
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
**Response (200 OK):**
|
|
420
|
+
```json
|
|
421
|
+
{
|
|
422
|
+
"message": "Sesión cerrada exitosamente"
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
---
|
|
427
|
+
|
|
428
|
+
### GET /api/v1/auth/me
|
|
429
|
+
|
|
430
|
+
**Middleware:** AuthMiddleware
|
|
431
|
+
|
|
432
|
+
**Request:**
|
|
433
|
+
```http
|
|
434
|
+
GET /api/v1/auth/me HTTP/1.1
|
|
435
|
+
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
**Response (200 OK):**
|
|
439
|
+
```json
|
|
440
|
+
{
|
|
441
|
+
"id": 123,
|
|
442
|
+
"email": "user@example.com",
|
|
443
|
+
"created_at": "2026-01-15T10:30:00Z"
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
---
|
|
448
|
+
|
|
449
|
+
## Seguridad
|
|
450
|
+
|
|
451
|
+
### Protecciones Implementadas
|
|
452
|
+
|
|
453
|
+
1. **Hashing de contraseñas:**
|
|
454
|
+
- Bcrypt con cost factor 12
|
|
455
|
+
- Nunca almacenar contraseñas en texto plano
|
|
456
|
+
- Verificación con timing-safe comparison
|
|
457
|
+
|
|
458
|
+
2. **Rate limiting:**
|
|
459
|
+
- 5 intentos/15min por IP+email
|
|
460
|
+
- Bloqueo temporal de 30 minutos
|
|
461
|
+
- No aplicar a logins exitosos
|
|
462
|
+
|
|
463
|
+
3. **JWT seguro:**
|
|
464
|
+
- Firma asimétrica (RS256)
|
|
465
|
+
- Claves RSA 4096 bits
|
|
466
|
+
- Payload mínimo (sin datos sensibles)
|
|
467
|
+
- Expiración obligatoria
|
|
468
|
+
|
|
469
|
+
4. **Headers de seguridad:**
|
|
470
|
+
```php
|
|
471
|
+
'X-Content-Type-Options' => 'nosniff',
|
|
472
|
+
'X-Frame-Options' => 'DENY',
|
|
473
|
+
'Content-Security-Policy' => "default-src 'self'",
|
|
474
|
+
'Strict-Transport-Security' => 'max-age=31536000; includeSubDomains'
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
5. **Logging y auditoría:**
|
|
478
|
+
- Todos los intentos registrados
|
|
479
|
+
- IP, user agent, timestamp
|
|
480
|
+
- Retención 90 días mínimo
|
|
481
|
+
|
|
482
|
+
### Vulnerabilidades Mitigadas
|
|
483
|
+
|
|
484
|
+
| Vulnerabilidad | Mitigación |
|
|
485
|
+
|----------------|-----------|
|
|
486
|
+
| Brute force | Rate limiting + bloqueo temporal |
|
|
487
|
+
| Timing attacks | Hash comparison constante |
|
|
488
|
+
| SQL injection | ORM + prepared statements |
|
|
489
|
+
| XSS | Sanitización de outputs |
|
|
490
|
+
| CSRF | Tokens CSRF en formularios web |
|
|
491
|
+
| Session hijacking | Tokens HttpOnly, HTTPS obligatorio |
|
|
492
|
+
| Enumeración de usuarios | Mensajes genéricos |
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
496
|
+
## Performance
|
|
497
|
+
|
|
498
|
+
### Benchmarks
|
|
499
|
+
|
|
500
|
+
| Operación | Tiempo (P95) |
|
|
501
|
+
|-----------|--------------|
|
|
502
|
+
| generateToken() | 15ms |
|
|
503
|
+
| validateToken() | 8ms |
|
|
504
|
+
| Hash::check() | 250ms |
|
|
505
|
+
| DB query (findByEmail) | 5ms |
|
|
506
|
+
| Total login | ~280ms |
|
|
507
|
+
|
|
508
|
+
### Optimizaciones
|
|
509
|
+
|
|
510
|
+
1. **Cachear public key:**
|
|
511
|
+
```php
|
|
512
|
+
Cache::remember('jwt_public_key', 3600, function () {
|
|
513
|
+
return file_get_contents(config('jwt.public_key_path'));
|
|
514
|
+
});
|
|
515
|
+
```
|
|
516
|
+
|
|
517
|
+
2. **Índices de BD:**
|
|
518
|
+
- `users.email` (UNIQUE)
|
|
519
|
+
- `login_attempts.email_ip` (compuesto)
|
|
520
|
+
- `login_attempts.attempted_at` (para queries con ventana de tiempo)
|
|
521
|
+
|
|
522
|
+
3. **Rate limiting en Redis (futuro):**
|
|
523
|
+
```php
|
|
524
|
+
// Mover de DB a Redis para mejor performance
|
|
525
|
+
Redis::incr("rate_limit:{$email}:{$ip}");
|
|
526
|
+
Redis::expire("rate_limit:{$email}:{$ip}", 900);
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
---
|
|
530
|
+
|
|
531
|
+
## Testing
|
|
532
|
+
|
|
533
|
+
### Unit Tests Requeridos
|
|
534
|
+
|
|
535
|
+
```php
|
|
536
|
+
// JWTServiceTest.php
|
|
537
|
+
test_generate_token_includes_correct_claims()
|
|
538
|
+
test_generate_token_with_remember_extends_expiry()
|
|
539
|
+
test_validate_token_with_valid_token()
|
|
540
|
+
test_validate_token_throws_on_expired_token()
|
|
541
|
+
test_validate_token_throws_on_tampered_signature()
|
|
542
|
+
|
|
543
|
+
// AuthServiceTest.php
|
|
544
|
+
test_authenticate_with_valid_credentials()
|
|
545
|
+
test_authenticate_with_invalid_password()
|
|
546
|
+
test_authenticate_with_nonexistent_email()
|
|
547
|
+
test_logout_revokes_token()
|
|
548
|
+
|
|
549
|
+
// RateLimitMiddlewareTest.php
|
|
550
|
+
test_allows_requests_under_limit()
|
|
551
|
+
test_blocks_after_max_attempts()
|
|
552
|
+
test_resets_after_lockout_period()
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
### Integration Tests Requeridos
|
|
556
|
+
|
|
557
|
+
```php
|
|
558
|
+
test_login_endpoint_returns_token()
|
|
559
|
+
test_login_endpoint_fails_with_invalid_credentials()
|
|
560
|
+
test_logout_endpoint_invalidates_token()
|
|
561
|
+
test_me_endpoint_returns_user_data()
|
|
562
|
+
test_protected_route_requires_valid_token()
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
---
|
|
566
|
+
|
|
567
|
+
## Configuración
|
|
568
|
+
|
|
569
|
+
### Variables de Entorno
|
|
570
|
+
|
|
571
|
+
```env
|
|
572
|
+
# JWT Configuration
|
|
573
|
+
JWT_PRIVATE_KEY_PATH=/var/www/storage/keys/private.pem
|
|
574
|
+
JWT_PUBLIC_KEY_PATH=/var/www/storage/keys/public.pem
|
|
575
|
+
JWT_TTL=86400
|
|
576
|
+
JWT_TTL_REMEMBER=2592000
|
|
577
|
+
JWT_ALGORITHM=RS256
|
|
578
|
+
|
|
579
|
+
# Rate Limiting
|
|
580
|
+
RATE_LIMIT_MAX_ATTEMPTS=5
|
|
581
|
+
RATE_LIMIT_DECAY_MINUTES=15
|
|
582
|
+
RATE_LIMIT_LOCKOUT_SECONDS=1800
|
|
583
|
+
|
|
584
|
+
# Security
|
|
585
|
+
BCRYPT_COST=12
|
|
586
|
+
HTTPS_ONLY=true
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
---
|
|
590
|
+
|
|
591
|
+
## Referencias
|
|
592
|
+
|
|
593
|
+
- JWT RFC 7519: https://tools.ietf.org/html/rfc7519
|
|
594
|
+
- RS256 Algorithm: https://tools.ietf.org/html/rfc7518#section-3.3
|
|
595
|
+
- OWASP Auth Cheatsheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
|
|
596
|
+
|
|
597
|
+
## Historial de Cambios
|
|
598
|
+
|
|
599
|
+
| Versión | Fecha | Autor | Cambios |
|
|
600
|
+
|---------|-------|-------|---------|
|
|
601
|
+
| 1.0 | 2026-02-12 | SpecLeap Contributor | Versión inicial implementada |
|
|
602
|
+
|
|
603
|
+
---
|
|
604
|
+
|
|
605
|
+
**Estado:** implemented
|
|
606
|
+
**Última revisión:** 2026-02-12
|