@jmlq/auth 0.0.1-alpha.15 → 0.0.1-alpha.18
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/dist/application/dtos/response/login.response.d.ts +1 -1
- package/dist/application/use-cases/refresh-token.use-case.js +9 -1
- package/dist/domain/entities/credential.entity.d.ts +27 -5
- package/dist/domain/entities/credential.entity.js +32 -3
- package/dist/domain/ports/repository/credential.repository.d.ts +15 -0
- package/dist/domain/props/entities/credential.props.d.ts +8 -1
- package/dist/in-memory/in-memory-credential.repository.d.ts +1 -0
- package/dist/in-memory/in-memory-credential.repository.js +9 -4
- package/dist/infrastructure/services/token-session.service.js +9 -13
- package/package.json +1 -1
|
@@ -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:
|
|
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,31 @@ 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
|
-
*
|
|
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;
|
|
91
|
+
/**
|
|
92
|
+
* Devuelve el refresh token o lanza si no existe.
|
|
93
|
+
*
|
|
94
|
+
* Uso:
|
|
95
|
+
* - Flujos donde el refresh token es OBLIGATORIO
|
|
96
|
+
* (login, refresh/rotación).
|
|
97
|
+
*
|
|
98
|
+
* Seguridad:
|
|
99
|
+
* - Evita usar `!` o cast inseguros.
|
|
100
|
+
* - Hace explícito el invariante del dominio.
|
|
101
|
+
*/
|
|
102
|
+
requireRefreshToken(): string;
|
|
81
103
|
}
|
|
@@ -83,12 +83,41 @@ class Credential {
|
|
|
83
83
|
}
|
|
84
84
|
/**
|
|
85
85
|
*Reconstitution method for repository
|
|
86
|
-
*
|
|
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
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Devuelve el refresh token o lanza si no existe.
|
|
107
|
+
*
|
|
108
|
+
* Uso:
|
|
109
|
+
* - Flujos donde el refresh token es OBLIGATORIO
|
|
110
|
+
* (login, refresh/rotación).
|
|
111
|
+
*
|
|
112
|
+
* Seguridad:
|
|
113
|
+
* - Evita usar `!` o cast inseguros.
|
|
114
|
+
* - Hace explícito el invariante del dominio.
|
|
115
|
+
*/
|
|
116
|
+
requireRefreshToken() {
|
|
117
|
+
if (!this._refreshToken) {
|
|
118
|
+
throw new Error("Invariant violation: refreshToken is required but missing");
|
|
119
|
+
}
|
|
120
|
+
return this._refreshToken;
|
|
121
|
+
}
|
|
93
122
|
}
|
|
94
123
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -1,4 +1,5 @@
|
|
|
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
5
|
const domain_1 = require("../../domain");
|
|
@@ -292,20 +293,15 @@ class TokenSessionService {
|
|
|
292
293
|
const { accessToken, refreshToken: newRefreshToken, expiresAt, } = await this.issueTokens(user, sessionId);
|
|
293
294
|
const rotated = domain_1.Credential.create(sessionId, user.id, accessToken, newRefreshToken, expiresAt);
|
|
294
295
|
/**
|
|
295
|
-
*
|
|
296
|
-
* -
|
|
297
|
-
* -
|
|
298
|
-
*
|
|
299
|
-
* Ventaja:
|
|
300
|
-
* - Funciona aunque no exista update/UPSERT
|
|
301
|
-
* - Mantiene invariantes del repo simples
|
|
302
|
-
*
|
|
303
|
-
* Consideración:
|
|
304
|
-
* - Si tu storage soporta transacciones, esto puede envolver en una transacción
|
|
305
|
-
* para evitar “ventana” de inconsistencia en sistemas altamente concurrentes.
|
|
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.
|
|
306
299
|
*/
|
|
307
|
-
await this.credentialRepository.
|
|
308
|
-
|
|
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
|
+
}
|
|
309
305
|
return rotated;
|
|
310
306
|
}
|
|
311
307
|
/**
|