@jmlq/auth 0.0.1-alpha.14 → 0.0.1-alpha.16

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.
@@ -1,5 +1,5 @@
1
1
  export interface LoginResponse {
2
2
  sessionId: string;
3
3
  accessToken: string;
4
- refreshToken: string;
4
+ refreshToken?: string;
5
5
  }
@@ -10,13 +10,21 @@ class RefreshTokenUseCase {
10
10
  try {
11
11
  // Refrescar la sesión
12
12
  const credential = await this.tokenSession.refreshSession(request.refreshToken);
13
+ // Rotación obligatoria: si no hay refreshToken nuevo, el refresh falló.
14
+ const newRefreshToken = credential.refreshToken;
15
+ if (!newRefreshToken || !newRefreshToken.trim()) {
16
+ // Mensaje genérico por seguridad
17
+ throw new errors_1.TokenExpiredError();
18
+ }
13
19
  return {
14
20
  sessionId: credential.sessionId.getValue(),
15
21
  accessToken: credential.accessToken,
16
- refreshToken: credential.refreshToken,
22
+ refreshToken: newRefreshToken,
17
23
  };
18
24
  }
19
25
  catch (error) {
26
+ // Si quieres, aquí puedes mapear SOLO expiración e invalid-token a TokenExpiredError
27
+ // y relanzar el resto. Mantengo tu comportamiento actual para no cambiar semántica.
20
28
  throw new errors_1.TokenExpiredError();
21
29
  }
22
30
  }
@@ -19,7 +19,7 @@ export declare class Credential {
19
19
  /**
20
20
  * Token de refresco asociado
21
21
  */
22
- private readonly _refreshToken;
22
+ private readonly _refreshToken?;
23
23
  /**
24
24
  * Fecha de expiración del token de acceso
25
25
  */
@@ -48,7 +48,7 @@ export declare class Credential {
48
48
  /**
49
49
  * Obtiene el token de refresco
50
50
  */
51
- get refreshToken(): string;
51
+ get refreshToken(): string | undefined;
52
52
  /**
53
53
  * Obtiene la fecha de expiración del token de acceso
54
54
  */
@@ -73,9 +73,19 @@ export declare class Credential {
73
73
  static create(sessionId: Id, userId: Id, accessToken: string, refreshToken: string, expirationDate: Date): Credential;
74
74
  /**
75
75
  *Reconstitution method for repository
76
- *Método usado por un repositorio para reconstruir la entidad desde la base de datos
77
- * @param props Propiedades de las credenciales
78
- * @returns Nueva instancia de Credential
76
+ *refreshToken puede venir undefined si en DB solo existe hash.
79
77
  */
80
78
  static reconstitute(props: ICredentialProps): Credential;
79
+ /**
80
+ * Crea una nueva credencial como resultado de una rotación de refresh token.
81
+ * Impone que refreshToken exista (rotación obligatoria).
82
+ */
83
+ static rotate(props: {
84
+ sessionId: Id;
85
+ userId: Id;
86
+ accessToken: string;
87
+ refreshToken: string;
88
+ expiresAt: Date;
89
+ createdAt?: Date;
90
+ }): Credential;
81
91
  }
@@ -83,12 +83,24 @@ class Credential {
83
83
  }
84
84
  /**
85
85
  *Reconstitution method for repository
86
- *Método usado por un repositorio para reconstruir la entidad desde la base de datos
87
- * @param props Propiedades de las credenciales
88
- * @returns Nueva instancia de Credential
86
+ *refreshToken puede venir undefined si en DB solo existe hash.
89
87
  */
90
88
  static reconstitute(props) {
91
89
  return new Credential(props);
92
90
  }
91
+ /**
92
+ * Crea una nueva credencial como resultado de una rotación de refresh token.
93
+ * Impone que refreshToken exista (rotación obligatoria).
94
+ */
95
+ static rotate(props) {
96
+ return new Credential({
97
+ sessionId: props.sessionId,
98
+ userId: props.userId,
99
+ accessToken: props.accessToken,
100
+ refreshToken: props.refreshToken,
101
+ expiresAt: props.expiresAt,
102
+ createdAt: props.createdAt ?? new Date(),
103
+ });
104
+ }
93
105
  }
94
106
  exports.Credential = Credential;
@@ -45,4 +45,19 @@ export interface ICredentialRepositoryPort {
45
45
  * Elimina una sesión por refresh token (logout basado en refresh).
46
46
  */
47
47
  deleteByRefreshToken(refreshToken: string): Promise<void>;
48
+ /**
49
+ * Rotación atómica de refresh token (single-use).
50
+ *
51
+ * Debe:
52
+ * - “consumir” el refreshToken actual (el entrante) de forma atómica,
53
+ * - y persistir la credencial nueva para la MISMA sessionId.
54
+ *
55
+ * Retorna true si la rotación ocurrió (1 fila afectada),
56
+ * false si el refresh token ya fue usado / revocado / no existe.
57
+ *
58
+ * Nota:
59
+ * - El core NO sabe de hashes.
60
+ * - La implementación infra puede usar refreshTokenHash internamente.
61
+ */
62
+ rotateByRefreshToken(currentRefreshToken: string, nextCredential: Credential): Promise<boolean>;
48
63
  }
@@ -3,7 +3,14 @@ export interface ICredentialProps {
3
3
  sessionId: Id;
4
4
  userId: Id;
5
5
  accessToken: string;
6
- refreshToken: string;
6
+ /**
7
+ * Token de refresco (secreto).
8
+ * Nota de seguridad:
9
+ * - En persistencia NO debe guardarse en claro.
10
+ * - En reconstitución desde DB puede ser undefined (solo existe el hash).
11
+ * - En flujos que emiten tokens (login/refresh) debe existir.
12
+ */
13
+ refreshToken?: string;
7
14
  expiresAt: Date;
8
15
  createdAt: Date;
9
16
  }
@@ -5,6 +5,7 @@ import { Credential, ICredentialRepositoryPort, Id } from "../domain";
5
5
  * Soporta múltiples sesiones por usuario (multi-dispositivo) usando `sessionId`.
6
6
  */
7
7
  export declare class InMemoryCredentialRepository implements ICredentialRepositoryPort {
8
+ rotateByRefreshToken(currentRefreshToken: string, nextCredential: Credential): Promise<boolean>;
8
9
  /**
9
10
  * Fuente de verdad: credenciales indexadas por sessionId.
10
11
  */
@@ -21,6 +21,9 @@ class InMemoryCredentialRepository {
21
21
  */
22
22
  this.refreshTokenIndex = new Map();
23
23
  }
24
+ rotateByRefreshToken(currentRefreshToken, nextCredential) {
25
+ throw new Error("Method not implemented.");
26
+ }
24
27
  /**
25
28
  * Guarda (upsert) una credencial por (userId, sessionId).
26
29
  * - Si existía una credencial para ese sessionId, limpia el índice de refreshToken viejo.
@@ -31,13 +34,14 @@ class InMemoryCredentialRepository {
31
34
  const userKey = credential.userId.getValue();
32
35
  // Si existía una credencial para esa sesión, limpiar refresh index anterior
33
36
  const old = this.credentialsBySessionId.get(sessionKey);
34
- if (old) {
37
+ if (old?.refreshToken) {
35
38
  this.refreshTokenIndex.delete(old.refreshToken);
36
39
  }
37
40
  // Guardar credencial por sesión
38
41
  this.credentialsBySessionId.set(sessionKey, credential);
39
42
  // Indexar refresh -> session
40
- this.refreshTokenIndex.set(credential.refreshToken, sessionKey);
43
+ if (credential.refreshToken)
44
+ this.refreshTokenIndex.set(credential.refreshToken, sessionKey);
41
45
  // Indexar user -> sessions
42
46
  const sessions = this.sessionsByUserId.get(userKey) ?? new Set();
43
47
  sessions.add(sessionKey);
@@ -97,7 +101,7 @@ class InMemoryCredentialRepository {
97
101
  return;
98
102
  for (const sid of sessionIds) {
99
103
  const c = this.credentialsBySessionId.get(sid);
100
- if (c) {
104
+ if (c?.refreshToken) {
101
105
  this.refreshTokenIndex.delete(c.refreshToken);
102
106
  }
103
107
  this.credentialsBySessionId.delete(sid);
@@ -113,7 +117,8 @@ class InMemoryCredentialRepository {
113
117
  if (!c)
114
118
  return;
115
119
  // limpiar refresh index
116
- this.refreshTokenIndex.delete(c.refreshToken);
120
+ if (c.refreshToken)
121
+ this.refreshTokenIndex.delete(c.refreshToken);
117
122
  // limpiar relación user -> sessions
118
123
  const userKey = c.userId.getValue();
119
124
  const sessions = this.sessionsByUserId.get(userKey);
@@ -2,23 +2,45 @@ import { Credential, ICredentialRepositoryPort, Id, ITokenServicePort, ITokenSes
2
2
  /**
3
3
  * Servicio de sesiones de usuario (rotación de refresh token) con soporte multi-dispositivo.
4
4
  *
5
- * Responsabilidad:
6
- * - Orquestar generación/verificación de tokens vía ITokenServicePort
7
- * - Persistir credenciales por sessionId (dispositivo)
8
- * - Recuperar usuario
5
+ * Capa:
6
+ * - Infraestructura (orquesta ports y persistencia, no contiene reglas de negocio de Auth)
9
7
  *
10
- * Reglas:
8
+ * Responsabilidades:
9
+ * - Generar/verificar tokens usando ITokenServicePort (plugin: jose / etc.)
10
+ * - Persistir credenciales por sessionId (1 fila = 1 sesión/dispositivo)
11
+ * - Recuperar usuario desde IUserRepositoryPort
12
+ *
13
+ * Reglas clave (multi-dispositivo):
11
14
  * - createSession: crea sessionId nuevo (nuevo dispositivo/sesión)
12
15
  * - refreshSession: rota tokens manteniendo el MISMO sessionId (misma sesión)
13
- * - validateSession: valida JWT y valida sesión activa en repositorio (revocable)
16
+ * - validateSession: valida JWT + valida que la sesión exista (revocable) y no expirada
14
17
  */
15
18
  export declare class TokenSessionService implements ITokenSessionPort {
19
+ /**
20
+ * Puerto para operaciones JWT (generar/verificar/expiración).
21
+ * Implementación típica: jose (via @jmlq/auth-plugin-jose).
22
+ */
16
23
  private readonly tokenService;
24
+ /**
25
+ * Puerto de usuarios (fuente de verdad del usuario).
26
+ * Se usa para:
27
+ * - encontrar usuario por id (payload.sub)
28
+ * - validar canLogin()
29
+ */
17
30
  private readonly userRepository;
31
+ /**
32
+ * Puerto de credenciales/sesiones (persistencia revocable).
33
+ * Se usa para:
34
+ * - save / findBySessionId / findByRefreshToken
35
+ * - deleteByRefreshToken / deleteBySessionId
36
+ */
18
37
  private readonly credentialRepository;
19
38
  /**
20
39
  * Expiración del access token en formato humano.
21
40
  * @example "15m"
41
+ *
42
+ * Nota:
43
+ * - La interpretación la hace el tokenService (o un parser interno).
22
44
  */
23
45
  private readonly accessTokenExpiration;
24
46
  /**
@@ -26,10 +48,32 @@ export declare class TokenSessionService implements ITokenSessionPort {
26
48
  * @example "7d"
27
49
  */
28
50
  private readonly refreshTokenExpiration;
29
- constructor(tokenService: ITokenServicePort, userRepository: IUserRepositoryPort, credentialRepository: ICredentialRepositoryPort,
51
+ constructor(
52
+ /**
53
+ * Puerto para operaciones JWT (generar/verificar/expiración).
54
+ * Implementación típica: jose (via @jmlq/auth-plugin-jose).
55
+ */
56
+ tokenService: ITokenServicePort,
57
+ /**
58
+ * Puerto de usuarios (fuente de verdad del usuario).
59
+ * Se usa para:
60
+ * - encontrar usuario por id (payload.sub)
61
+ * - validar canLogin()
62
+ */
63
+ userRepository: IUserRepositoryPort,
64
+ /**
65
+ * Puerto de credenciales/sesiones (persistencia revocable).
66
+ * Se usa para:
67
+ * - save / findBySessionId / findByRefreshToken
68
+ * - deleteByRefreshToken / deleteBySessionId
69
+ */
70
+ credentialRepository: ICredentialRepositoryPort,
30
71
  /**
31
72
  * Expiración del access token en formato humano.
32
73
  * @example "15m"
74
+ *
75
+ * Nota:
76
+ * - La interpretación la hace el tokenService (o un parser interno).
33
77
  */
34
78
  accessTokenExpiration?: string,
35
79
  /**
@@ -40,6 +84,19 @@ export declare class TokenSessionService implements ITokenSessionPort {
40
84
  /**
41
85
  * Emite access/refresh tokens para un usuario y una sesión dada.
42
86
  *
87
+ * Este método es el “núcleo” de emisión de tokens y su claim policy.
88
+ *
89
+ * Qué hace:
90
+ * 1) Construye claims del usuario (id/email/roles)
91
+ * 2) Deriva permissions efectivas desde roles (RBAC)
92
+ * 3) Construye customClaims.permissions para que el API pueda autorizar (requirePermissions)
93
+ * 4) Genera access + refresh
94
+ * 5) Obtiene expiresAt desde el accessToken (fuente robusta: tokenService)
95
+ *
96
+ * Importante:
97
+ * - permissions vienen de roles.getValuePublic().permissions
98
+ * (en el host se “pegan” desde AccessSnapshotResolver)
99
+ *
43
100
  * @param user Usuario autenticado.
44
101
  * @param sessionId Identificador de sesión/dispositivo.
45
102
  */
@@ -47,6 +104,15 @@ export declare class TokenSessionService implements ITokenSessionPort {
47
104
  /**
48
105
  * Crea una nueva sesión (nuevo dispositivo).
49
106
  *
107
+ * Flujo:
108
+ * 1) Genera un sessionId nuevo
109
+ * 2) Emite tokens atados a ese sessionId
110
+ * 3) Construye Credential (entidad de dominio de sesión)
111
+ * 4) Persiste credencial
112
+ *
113
+ * Efecto:
114
+ * - multi-dispositivo: un usuario puede tener N credenciales activas (N sessionId)
115
+ *
50
116
  * @param user Usuario autenticado.
51
117
  * @returns Credencial persistida para la nueva sesión.
52
118
  */
@@ -54,6 +120,17 @@ export declare class TokenSessionService implements ITokenSessionPort {
54
120
  /**
55
121
  * Rota refresh token manteniendo el MISMO sessionId (misma sesión/dispositivo).
56
122
  *
123
+ * Flujo:
124
+ * 1) Busca credencial por refreshToken (revocable)
125
+ * 2) Verifica refreshToken (firma/exp)
126
+ * 3) Recupera usuario (payload.sub) y valida canLogin()
127
+ * 4) Re-emite tokens con el MISMO sessionId
128
+ * 5) Reemplaza credencial persistida (delete + save)
129
+ *
130
+ * Seguridad:
131
+ * - Si el refreshToken no existe en repositorio => sesión revocada o token inválido => TokenExpiredError
132
+ * - Verificación JWT también puede lanzar => TokenExpiredError
133
+ *
57
134
  * @param refreshToken Refresh token actual.
58
135
  * @returns Nueva credencial rotada para la misma sesión.
59
136
  */
@@ -61,11 +138,15 @@ export declare class TokenSessionService implements ITokenSessionPort {
61
138
  /**
62
139
  * Valida una sesión a partir de access token.
63
140
  *
141
+ * Este método soporta “revocación” (token válido criptográficamente pero sesión eliminada en DB).
142
+ *
64
143
  * Reglas:
65
144
  * - JWT debe ser válido (firma/exp/issuer/aud según plugin)
66
145
  * - Debe existir `sid` en el payload
67
- * - La sesión (sid) debe existir en el repositorio (revocable)
68
- * - (opcional pero recomendado) el accessToken debe coincidir con el almacenado
146
+ * - La sesión (sid) debe existir en el repositorio
147
+ * - (recomendado) accessToken debe coincidir con el almacenado (solo el último token es válido)
148
+ * - La credencial no debe estar expirada (según su estado interno)
149
+ * - El usuario debe existir y poder loguearse
69
150
  *
70
151
  * @param accessToken Access token recibido.
71
152
  * @returns Usuario si sesión es válida, o null si no lo es.
@@ -74,12 +155,22 @@ export declare class TokenSessionService implements ITokenSessionPort {
74
155
  /**
75
156
  * Revoca una sesión usando refresh token.
76
157
  *
158
+ * Uso típico:
159
+ * - logout donde el cliente envía refreshToken (cookie/body)
160
+ *
161
+ * Efecto:
162
+ * - la sesión deja de poder refrescar tokens
163
+ * - si validateSession compara accessToken, también invalida el access actual cuando se borra la credencial
164
+ *
77
165
  * @param refreshToken Refresh token actual.
78
166
  */
79
167
  revokeSession(refreshToken: string): Promise<void>;
80
168
  /**
81
169
  * Revoca una sesión por sessionId (logout por dispositivo).
82
170
  *
171
+ * Uso típico:
172
+ * - logout selectivo (cerrar solo este dispositivo)
173
+ *
83
174
  * @param sessionId Identificador de sesión/dispositivo.
84
175
  */
85
176
  revokeSessionById(sessionId: Id): Promise<void>;
@@ -1,8 +1,48 @@
1
1
  "use strict";
2
+ // src/infrastructure/services/token-session.service.ts
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  exports.TokenSessionService = void 0;
4
- // src/infrastructure/services/token-session.service.ts
5
5
  const domain_1 = require("../../domain");
6
+ /**
7
+ * Type guard para validar arrays de strings en runtime.
8
+ *
9
+ * Se usa para:
10
+ * - evitar suposiciones sobre shapes provenientes de getValuePublic()
11
+ * - garantizar que "permissions" sea string[] antes de usarlo en claims
12
+ */
13
+ function isStringArray(value) {
14
+ return Array.isArray(value) && value.every((v) => typeof v === "string");
15
+ }
16
+ /**
17
+ * Devuelve valores únicos y ordenados (estable y determinista).
18
+ *
19
+ * Por qué importa:
20
+ * - Evita duplicados en permissions (p.ej. permisos compartidos entre roles)
21
+ * - Orden estable => tokens más previsibles y facilita testing/debug
22
+ */
23
+ function uniqSorted(values) {
24
+ return Array.from(new Set(values)).sort((a, b) => a.localeCompare(b));
25
+ }
26
+ /**
27
+ * Extrae permissions[] desde el resultado de getValuePublic() de los roles.
28
+ *
29
+ * Diseño:
30
+ * - No asume que permissions existe ni que tiene el tipo correcto.
31
+ * - Solo agrega cuando es string[].
32
+ * - Normaliza con uniqSorted().
33
+ *
34
+ * Resultado:
35
+ * - permissions finales efectivas para el usuario en ese instante.
36
+ */
37
+ function extractPermissionsFromRolesPublic(rolesPublic) {
38
+ const perms = [];
39
+ for (const rp of rolesPublic) {
40
+ if (isStringArray(rp.permissions)) {
41
+ perms.push(...rp.permissions);
42
+ }
43
+ }
44
+ return uniqSorted(perms);
45
+ }
6
46
  /**
7
47
  * Extrae el `sid` (sessionId) del payload de forma segura.
8
48
  *
@@ -10,30 +50,61 @@ const domain_1 = require("../../domain");
10
50
  * - En la mayoría de implementaciones, `sid` viene como claim plano.
11
51
  * - Si tu `ITokenServicePort` lo entrega de otra forma, ajusta aquí (único lugar).
12
52
  *
53
+ * Ventaja:
54
+ * - Centraliza la dependencia en el formato del payload (single source of truth).
55
+ *
13
56
  * @param payload Payload verificado del JWT.
14
- * @returns sessionId (sid) o undefined si no existe.
57
+ * @returns sessionId (sid).
15
58
  */
16
59
  function getSessionIdFromPayload(payload) {
60
+ // Aquí se asume que IJWTPayload expone "sid" como string.
61
+ // Si llegara a ser opcional en el tipo, este método debería retornar string | undefined
62
+ // y el resto del flujo ya está preparado para manejar "falsy".
17
63
  return payload.sid;
18
64
  }
19
65
  /**
20
66
  * Servicio de sesiones de usuario (rotación de refresh token) con soporte multi-dispositivo.
21
67
  *
22
- * Responsabilidad:
23
- * - Orquestar generación/verificación de tokens vía ITokenServicePort
24
- * - Persistir credenciales por sessionId (dispositivo)
25
- * - Recuperar usuario
68
+ * Capa:
69
+ * - Infraestructura (orquesta ports y persistencia, no contiene reglas de negocio de Auth)
70
+ *
71
+ * Responsabilidades:
72
+ * - Generar/verificar tokens usando ITokenServicePort (plugin: jose / etc.)
73
+ * - Persistir credenciales por sessionId (1 fila = 1 sesión/dispositivo)
74
+ * - Recuperar usuario desde IUserRepositoryPort
26
75
  *
27
- * Reglas:
76
+ * Reglas clave (multi-dispositivo):
28
77
  * - createSession: crea sessionId nuevo (nuevo dispositivo/sesión)
29
78
  * - refreshSession: rota tokens manteniendo el MISMO sessionId (misma sesión)
30
- * - validateSession: valida JWT y valida sesión activa en repositorio (revocable)
79
+ * - validateSession: valida JWT + valida que la sesión exista (revocable) y no expirada
31
80
  */
32
81
  class TokenSessionService {
33
- constructor(tokenService, userRepository, credentialRepository,
82
+ constructor(
83
+ /**
84
+ * Puerto para operaciones JWT (generar/verificar/expiración).
85
+ * Implementación típica: jose (via @jmlq/auth-plugin-jose).
86
+ */
87
+ tokenService,
88
+ /**
89
+ * Puerto de usuarios (fuente de verdad del usuario).
90
+ * Se usa para:
91
+ * - encontrar usuario por id (payload.sub)
92
+ * - validar canLogin()
93
+ */
94
+ userRepository,
95
+ /**
96
+ * Puerto de credenciales/sesiones (persistencia revocable).
97
+ * Se usa para:
98
+ * - save / findBySessionId / findByRefreshToken
99
+ * - deleteByRefreshToken / deleteBySessionId
100
+ */
101
+ credentialRepository,
34
102
  /**
35
103
  * Expiración del access token en formato humano.
36
104
  * @example "15m"
105
+ *
106
+ * Nota:
107
+ * - La interpretación la hace el tokenService (o un parser interno).
37
108
  */
38
109
  accessTokenExpiration = "15m",
39
110
  /**
@@ -50,38 +121,119 @@ class TokenSessionService {
50
121
  /**
51
122
  * Emite access/refresh tokens para un usuario y una sesión dada.
52
123
  *
124
+ * Este método es el “núcleo” de emisión de tokens y su claim policy.
125
+ *
126
+ * Qué hace:
127
+ * 1) Construye claims del usuario (id/email/roles)
128
+ * 2) Deriva permissions efectivas desde roles (RBAC)
129
+ * 3) Construye customClaims.permissions para que el API pueda autorizar (requirePermissions)
130
+ * 4) Genera access + refresh
131
+ * 5) Obtiene expiresAt desde el accessToken (fuente robusta: tokenService)
132
+ *
133
+ * Importante:
134
+ * - permissions vienen de roles.getValuePublic().permissions
135
+ * (en el host se “pegan” desde AccessSnapshotResolver)
136
+ *
53
137
  * @param user Usuario autenticado.
54
138
  * @param sessionId Identificador de sesión/dispositivo.
55
139
  */
56
140
  async issueTokens(user, sessionId) {
141
+ /**
142
+ * rolesPublic:
143
+ * - getValuePublic() expone un shape serializable del Role
144
+ * - se tipa como RolePublic[] vía cast (controlado) para extraer permissions en runtime
145
+ *
146
+ * Nota:
147
+ * - Este cast está contenido aquí.
148
+ * - Las comprobaciones reales se hacen con isStringArray().
149
+ */
150
+ const rolesPublic = user.roles.map((r) => r.getValuePublic());
151
+ /**
152
+ * permissions vienen “pegadas” a los roles por el host (API REST):
153
+ * - AccessSnapshotResolver resuelve roles + permissions actuales
154
+ * - mapRolesFromSnapshot crea Role(role, Permission[]) y getValuePublic expone permissions
155
+ */
156
+ const permissions = extractPermissionsFromRolesPublic(rolesPublic);
157
+ /**
158
+ * Custom claims mínimos que necesita el API para requirePermissions().
159
+ *
160
+ * Decisión:
161
+ * - Si no hay permissions => customClaims vacío (JWT más limpio).
162
+ * - Si hay permissions => { permissions: string[] }.
163
+ *
164
+ * Esto hace que requirePermissions() pueda operar solo con JWT,
165
+ * sin requerir llamadas extra al resolver en cada request.
166
+ */
167
+ const customClaims = permissions.length > 0 ? { permissions } : {};
168
+ /**
169
+ * userClaims:
170
+ * - Shape mínimo de usuario que el tokenService necesita
171
+ * - Evita pasar la entidad User completa (no serializable y con lógica)
172
+ */
57
173
  const userClaims = {
58
174
  id: user.id.getValue(),
59
175
  email: user.email.getValue(),
60
176
  roles: user.roles.map((r) => r.getValuePublic()),
61
177
  };
178
+ /**
179
+ * Generar access token:
180
+ * - sessionId se coloca en "sid"
181
+ * - expiresIn aplica política de expiración configurada
182
+ * - customClaims adjunta permissions para autorización en el API
183
+ */
62
184
  const accessToken = await this.tokenService.generateAccessToken({
63
185
  user: userClaims,
64
186
  expiresIn: this.accessTokenExpiration,
65
187
  sessionId: sessionId.getValue(),
188
+ customClaims,
66
189
  });
190
+ /**
191
+ * Generar refresh token:
192
+ * - Mantiene mismo sid
193
+ * - Suele tener expiración mayor
194
+ * - También transporta customClaims (depende de tu política; aquí se incluye por consistencia)
195
+ */
67
196
  const refreshToken = await this.tokenService.generateRefreshToken({
68
197
  user: userClaims,
69
198
  expiresIn: this.refreshTokenExpiration,
70
199
  sessionId: sessionId.getValue(),
200
+ customClaims,
71
201
  });
72
- // Obtener expiración desde el token (robusto, sin depender de parseos propios)
202
+ /**
203
+ * Expiración del access token:
204
+ * - Se deriva desde el token (tokenService), evitando parseos propios
205
+ * - Reduce riesgo de inconsistencias entre "exp" y cálculos internos
206
+ */
73
207
  const expiresAt = await this.tokenService.getTokenExpiration(accessToken);
74
208
  return { accessToken, refreshToken, expiresAt };
75
209
  }
76
210
  /**
77
211
  * Crea una nueva sesión (nuevo dispositivo).
78
212
  *
213
+ * Flujo:
214
+ * 1) Genera un sessionId nuevo
215
+ * 2) Emite tokens atados a ese sessionId
216
+ * 3) Construye Credential (entidad de dominio de sesión)
217
+ * 4) Persiste credencial
218
+ *
219
+ * Efecto:
220
+ * - multi-dispositivo: un usuario puede tener N credenciales activas (N sessionId)
221
+ *
79
222
  * @param user Usuario autenticado.
80
223
  * @returns Credencial persistida para la nueva sesión.
81
224
  */
82
225
  async createSession(user) {
83
226
  const sessionId = domain_1.Id.generate();
84
227
  const { accessToken, refreshToken, expiresAt } = await this.issueTokens(user, sessionId);
228
+ /**
229
+ * Credential representa la sesión activa.
230
+ * Suele incluir:
231
+ * - sessionId
232
+ * - userId
233
+ * - accessToken actual
234
+ * - refreshToken actual
235
+ * - expiresAt (del access)
236
+ */
85
237
  const credential = domain_1.Credential.create(sessionId, user.id, accessToken, refreshToken, expiresAt);
86
238
  await this.credentialRepository.save(credential);
87
239
  return credential;
@@ -89,75 +241,151 @@ class TokenSessionService {
89
241
  /**
90
242
  * Rota refresh token manteniendo el MISMO sessionId (misma sesión/dispositivo).
91
243
  *
244
+ * Flujo:
245
+ * 1) Busca credencial por refreshToken (revocable)
246
+ * 2) Verifica refreshToken (firma/exp)
247
+ * 3) Recupera usuario (payload.sub) y valida canLogin()
248
+ * 4) Re-emite tokens con el MISMO sessionId
249
+ * 5) Reemplaza credencial persistida (delete + save)
250
+ *
251
+ * Seguridad:
252
+ * - Si el refreshToken no existe en repositorio => sesión revocada o token inválido => TokenExpiredError
253
+ * - Verificación JWT también puede lanzar => TokenExpiredError
254
+ *
92
255
  * @param refreshToken Refresh token actual.
93
256
  * @returns Nueva credencial rotada para la misma sesión.
94
257
  */
95
258
  async refreshSession(refreshToken) {
96
259
  const existing = await this.credentialRepository.findByRefreshToken(refreshToken);
260
+ // Si no existe el refreshToken en DB, se considera expirado/revocado
97
261
  if (!existing)
98
262
  throw new domain_1.TokenExpiredError();
263
+ /**
264
+ * Verificación criptográfica y claims:
265
+ * - Si la firma o exp fallan => TokenExpiredError
266
+ *
267
+ * Nota:
268
+ * - Se oculta el error real por seguridad (no filtrar detalles al cliente).
269
+ */
99
270
  const payload = await this.tokenService
100
271
  .verifyRefreshToken(refreshToken)
101
272
  .catch(() => {
102
273
  throw new domain_1.TokenExpiredError();
103
274
  });
275
+ /**
276
+ * El sub del payload identifica al usuario.
277
+ * Se re-carga para:
278
+ * - validar existencia
279
+ * - validar estado (canLogin)
280
+ * - obtener roles/permisos actuales (si el repo resuelve RBAC en cada fetch)
281
+ */
104
282
  const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
105
283
  if (!user)
106
284
  throw new domain_1.UserNotFoundError();
107
285
  if (!user.canLogin())
108
286
  throw new domain_1.UserDisabledError();
109
- // Mantener identidad de sesión (dispositivo)
287
+ /**
288
+ * Mantener identidad de sesión:
289
+ * - NO se crea un sessionId nuevo
290
+ * - Se rota token, pero el dispositivo/sesión sigue siendo el mismo
291
+ */
110
292
  const sessionId = existing.sessionId;
111
293
  const { accessToken, refreshToken: newRefreshToken, expiresAt, } = await this.issueTokens(user, sessionId);
112
294
  const rotated = domain_1.Credential.create(sessionId, user.id, accessToken, newRefreshToken, expiresAt);
113
295
  /**
114
- * Estrategia simple (sin sobreingeniería):
115
- * - Eliminar la sesión por sessionId
116
- * - Guardar la nueva credencial rotada (misma sesión)
117
- *
118
- * Alternativa: credentialRepository.update(rotated)
119
- * (si tu implementación hace UPSERT, `save` ya cubre el caso)
296
+ * Rotación atómica (single-use):
297
+ * - Si otro request ya consumió este refreshToken, esta operación debe fallar.
298
+ * - Evita que el mismo refreshToken se use dos veces bajo concurrencia.
120
299
  */
121
- await this.credentialRepository.deleteBySessionId(sessionId);
122
- await this.credentialRepository.save(rotated);
300
+ const rotatedOk = await this.credentialRepository.rotateByRefreshToken(refreshToken, rotated);
301
+ if (!rotatedOk) {
302
+ // Token ya consumido/revocado/no existe (no filtramos detalle)
303
+ throw new domain_1.TokenExpiredError();
304
+ }
123
305
  return rotated;
124
306
  }
125
307
  /**
126
308
  * Valida una sesión a partir de access token.
127
309
  *
310
+ * Este método soporta “revocación” (token válido criptográficamente pero sesión eliminada en DB).
311
+ *
128
312
  * Reglas:
129
313
  * - JWT debe ser válido (firma/exp/issuer/aud según plugin)
130
314
  * - Debe existir `sid` en el payload
131
- * - La sesión (sid) debe existir en el repositorio (revocable)
132
- * - (opcional pero recomendado) el accessToken debe coincidir con el almacenado
315
+ * - La sesión (sid) debe existir en el repositorio
316
+ * - (recomendado) accessToken debe coincidir con el almacenado (solo el último token es válido)
317
+ * - La credencial no debe estar expirada (según su estado interno)
318
+ * - El usuario debe existir y poder loguearse
133
319
  *
134
320
  * @param accessToken Access token recibido.
135
321
  * @returns Usuario si sesión es válida, o null si no lo es.
136
322
  */
137
323
  async validateSession(accessToken) {
138
324
  try {
325
+ /**
326
+ * verifyAccessToken:
327
+ * - valida firma y exp y claims críticos según configuración del tokenService
328
+ * - retorna payload tipado como IJWTPayload
329
+ */
139
330
  const payload = await this.tokenService.verifyAccessToken(accessToken);
331
+ /**
332
+ * sid:
333
+ * - identifica la sesión/dispositivo
334
+ * - si no existe, no podemos validar sesión revocable => null
335
+ */
140
336
  const sid = getSessionIdFromPayload(payload);
141
337
  if (!sid)
142
338
  return null;
339
+ /**
340
+ * Validación de sesión en storage:
341
+ * - si no existe => sesión revocada => null
342
+ */
143
343
  const credential = await this.credentialRepository.findBySessionId(new domain_1.Id(sid));
144
344
  if (!credential)
145
345
  return null;
146
- // Recomendado: asegura que solo el último access token de esa sesión sea válido
346
+ /**
347
+ * Validación recomendada:
348
+ * - asegura que solo el último accessToken emitido para esa sesión sea aceptado
349
+ * - previene replay de accessTokens antiguos dentro de la misma sesión
350
+ *
351
+ * Nota:
352
+ * - Esto exige que el repo persista accessToken actual.
353
+ */
147
354
  if (credential.accessToken !== accessToken)
148
355
  return null;
356
+ /**
357
+ * Validación de expiración a nivel de credencial:
358
+ * - respaldo adicional (además de "exp" del JWT)
359
+ * - útil si tu sistema modela expiración o revocación interna
360
+ */
149
361
  if (credential.isExpired())
150
362
  return null;
363
+ /**
364
+ * sub del payload identifica al usuario.
365
+ * Se valida existencia y estado.
366
+ */
151
367
  const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
152
368
  return user && user.canLogin() ? user : null;
153
369
  }
154
370
  catch (e) {
371
+ /**
372
+ * SessionAuthError:
373
+ * - encapsula el error interno (logging/observabilidad)
374
+ * - evita filtrar detalles sensibles al exterior
375
+ */
155
376
  throw new domain_1.SessionAuthError("Session Authentication failed", e);
156
377
  }
157
378
  }
158
379
  /**
159
380
  * Revoca una sesión usando refresh token.
160
381
  *
382
+ * Uso típico:
383
+ * - logout donde el cliente envía refreshToken (cookie/body)
384
+ *
385
+ * Efecto:
386
+ * - la sesión deja de poder refrescar tokens
387
+ * - si validateSession compara accessToken, también invalida el access actual cuando se borra la credencial
388
+ *
161
389
  * @param refreshToken Refresh token actual.
162
390
  */
163
391
  async revokeSession(refreshToken) {
@@ -166,6 +394,9 @@ class TokenSessionService {
166
394
  /**
167
395
  * Revoca una sesión por sessionId (logout por dispositivo).
168
396
  *
397
+ * Uso típico:
398
+ * - logout selectivo (cerrar solo este dispositivo)
399
+ *
169
400
  * @param sessionId Identificador de sesión/dispositivo.
170
401
  */
171
402
  async revokeSessionById(sessionId) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@jmlq/auth",
3
3
  "description": "JWT authentication package with clean architecture",
4
- "version": "0.0.1-alpha.14",
4
+ "version": "0.0.1-alpha.16",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {