@jmlq/auth 0.0.1-alpha.8 → 0.0.1-beta.1
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/README.es.md +133 -0
- package/README.md +81 -207
- package/dist/application/dtos/index.d.ts +1 -1
- package/dist/application/dtos/index.js +1 -1
- package/dist/application/dtos/request/change-password.request.d.ts +15 -0
- package/dist/application/dtos/request/index.d.ts +5 -0
- package/dist/application/dtos/request/index.js +5 -0
- package/dist/application/dtos/request/logout.request.d.ts +2 -1
- package/dist/application/dtos/request/me.request.d.ts +3 -0
- package/dist/application/dtos/request/me.request.js +2 -0
- package/dist/application/dtos/request/register-user.request.d.ts +2 -1
- package/dist/application/dtos/request/request-password-reset.request.d.ts +6 -0
- package/dist/application/dtos/request/request-password-reset.request.js +2 -0
- package/dist/application/dtos/request/reset-password.request.d.ts +14 -0
- package/dist/application/dtos/request/reset-password.request.js +2 -0
- package/dist/application/dtos/request/verify-email.request.d.ts +3 -0
- package/dist/application/dtos/request/verify-email.request.js +2 -0
- package/dist/application/dtos/response/change-password.response.d.ts +7 -0
- package/dist/application/dtos/response/change-password.response.js +2 -0
- package/dist/application/dtos/response/index.d.ts +5 -0
- package/dist/application/dtos/response/index.js +5 -0
- package/dist/application/dtos/response/login.response.d.ts +2 -1
- package/dist/application/dtos/response/me.response.d.ts +11 -0
- package/dist/application/dtos/response/me.response.js +2 -0
- package/dist/application/dtos/response/refresh-token.response.d.ts +1 -0
- package/dist/application/dtos/response/register-user.response.d.ts +9 -0
- package/dist/application/dtos/response/request-password-reset.response.d.ts +15 -0
- package/dist/application/dtos/response/request-password-reset.response.js +2 -0
- package/dist/application/dtos/response/reset-password.response.d.ts +7 -0
- package/dist/application/dtos/response/reset-password.response.js +2 -0
- package/dist/application/dtos/response/verify-email.response.d.ts +4 -0
- package/dist/application/dtos/response/verify-email.response.js +2 -0
- package/dist/application/dtos/types/user-role.type.js +2 -0
- package/dist/application/facades/auth.facade.d.ts +33 -0
- package/dist/application/facades/auth.facade.js +60 -0
- package/dist/application/facades/create-auth-facade.d.ts +22 -0
- package/dist/application/facades/create-auth-facade.js +9 -0
- package/dist/application/facades/index.d.ts +2 -0
- package/dist/application/facades/index.js +18 -0
- package/dist/application/factories/auth-service.factory.d.ts +4 -4
- package/dist/application/factories/auth-service.factory.js +16 -4
- package/dist/application/factories/index.js +1 -0
- package/dist/application/types/auth-service-factory-options.type.d.ts +44 -0
- package/dist/application/use-cases/change-password.use-case.d.ts +21 -0
- package/dist/application/use-cases/change-password.use-case.js +49 -0
- package/dist/application/use-cases/index.d.ts +5 -0
- package/dist/application/use-cases/index.js +5 -0
- package/dist/application/use-cases/internal/index.d.ts +1 -0
- package/dist/application/use-cases/internal/index.js +17 -0
- package/dist/application/use-cases/internal/password-assertions.d.ts +13 -0
- package/dist/application/use-cases/internal/password-assertions.js +26 -0
- package/dist/application/use-cases/login-with-password.use-case.js +24 -28
- package/dist/application/use-cases/logout.use-case.js +14 -2
- package/dist/application/use-cases/me.use-case.d.ts +7 -0
- package/dist/application/use-cases/me.use-case.js +28 -0
- package/dist/application/use-cases/refresh-token.use-case.d.ts +16 -2
- package/dist/application/use-cases/refresh-token.use-case.js +33 -5
- package/dist/application/use-cases/register-user.use-case.d.ts +6 -1
- package/dist/application/use-cases/register-user.use-case.js +18 -7
- package/dist/application/use-cases/request-password-reset.use-case.d.ts +19 -0
- package/dist/application/use-cases/request-password-reset.use-case.js +43 -0
- package/dist/application/use-cases/reset-password.use-case.d.ts +20 -0
- package/dist/application/use-cases/reset-password.use-case.js +59 -0
- package/dist/application/use-cases/verify-email.use-case.d.ts +8 -0
- package/dist/application/use-cases/verify-email.use-case.js +30 -0
- package/dist/domain/entities/credential.entity.d.ts +36 -11
- package/dist/domain/entities/credential.entity.js +41 -11
- package/dist/domain/entities/user.entity.d.ts +32 -1
- package/dist/domain/entities/user.entity.js +54 -1
- package/dist/domain/errors/auth-error-code.d.ts +16 -0
- package/dist/domain/errors/auth-error-code.js +60 -0
- package/dist/domain/errors/auth.errors.d.ts +29 -8
- package/dist/domain/errors/auth.errors.js +59 -8
- package/dist/domain/errors/general.errors.d.ts +4 -0
- package/dist/domain/errors/general.errors.js +10 -0
- package/dist/domain/errors/identity.errors.d.ts +17 -12
- package/dist/domain/errors/identity.errors.js +29 -25
- package/dist/domain/errors/index.d.ts +5 -0
- package/dist/domain/errors/index.js +5 -0
- package/dist/domain/errors/jwt-payload.error.d.ts +4 -0
- package/dist/domain/errors/jwt-payload.error.js +10 -0
- package/dist/domain/errors/jwt.errors.d.ts +7 -0
- package/dist/domain/errors/jwt.errors.js +16 -0
- package/dist/domain/errors/password-reset.errors.d.ts +14 -0
- package/dist/domain/errors/password-reset.errors.js +29 -0
- package/dist/domain/index.d.ts +1 -0
- package/dist/domain/index.js +1 -0
- package/dist/domain/ports/auth/email-verification-token.port.d.ts +19 -0
- package/dist/domain/ports/auth/email-verification-token.port.js +2 -0
- package/dist/domain/ports/auth/index.d.ts +2 -0
- package/dist/domain/ports/auth/index.js +2 -0
- package/dist/domain/ports/auth/password-reset-token.port.d.ts +36 -0
- package/dist/domain/ports/auth/password-reset-token.port.js +2 -0
- package/dist/domain/ports/jwt/payload/jwt-payload.port.d.ts +25 -3
- package/dist/domain/ports/repository/credential.repository.d.ts +55 -2
- package/dist/domain/ports/token/token-session.port.d.ts +2 -0
- package/dist/domain/props/entities/credential.props.d.ts +9 -1
- package/dist/domain/props/entities/user.props.d.ts +1 -0
- package/dist/domain/props/jwt/generate-access-token.props.d.ts +3 -2
- package/dist/domain/props/jwt/generate-refresh-token.props.d.ts +3 -2
- package/dist/domain/props/jwt/jwt-user.d.ts +11 -2
- package/dist/domain/services/helpers/assert-jwt-structure.helper.d.ts +1 -0
- package/dist/domain/services/helpers/assert-jwt-structure.helper.js +13 -0
- package/dist/domain/services/helpers/index.d.ts +7 -0
- package/dist/domain/services/helpers/index.js +23 -0
- package/dist/domain/services/helpers/optional-audience.helper.d.ts +14 -0
- package/dist/domain/services/helpers/optional-audience.helper.js +49 -0
- package/dist/domain/services/helpers/optional-non-empty-string.helper.d.ts +1 -0
- package/dist/domain/services/helpers/optional-non-empty-string.helper.js +9 -0
- package/dist/domain/services/helpers/optional-record.helper.d.ts +1 -0
- package/dist/domain/services/helpers/optional-record.helper.js +15 -0
- package/dist/domain/services/helpers/optional-roles.helper.d.ts +3 -0
- package/dist/domain/services/helpers/optional-roles.helper.js +32 -0
- package/dist/domain/services/helpers/require-finite-number.helper.d.ts +1 -0
- package/dist/domain/services/helpers/require-finite-number.helper.js +12 -0
- package/dist/domain/services/helpers/require-non-empty-string.helper.d.ts +1 -0
- package/dist/domain/services/helpers/require-non-empty-string.helper.js +12 -0
- package/dist/domain/services/index.d.ts +1 -0
- package/dist/domain/services/index.js +1 -0
- package/dist/domain/services/normalize-jwt-payload.service.d.ts +19 -0
- package/dist/domain/services/normalize-jwt-payload.service.js +58 -0
- package/dist/domain/types/access-snapshot.type.d.ts +15 -0
- package/dist/domain/types/access-snapshot.type.js +2 -0
- package/dist/domain/types/index.d.ts +1 -0
- package/dist/domain/types/index.js +2 -0
- package/dist/in-memory/in-memory-credential.repository.d.ts +66 -3
- package/dist/in-memory/in-memory-credential.repository.js +174 -46
- package/dist/index.d.ts +18 -2
- package/dist/index.js +24 -9
- package/dist/infrastructure/index.d.ts +3 -0
- package/dist/infrastructure/index.js +18 -0
- package/dist/infrastructure/security/bcrypt-password-hasher.js +0 -1
- package/dist/infrastructure/services/token-session.service.d.ts +163 -8
- package/dist/infrastructure/services/token-session.service.js +290 -37
- package/dist/infrastructure/types/auth-service-container.d.ts +21 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/jwt-plugin/create-jwt-id.d.ts +6 -0
- package/dist/shared/jwt-plugin/create-jwt-id.js +30 -0
- package/dist/shared/jwt-plugin/index.d.ts +9 -0
- package/dist/shared/jwt-plugin/index.js +25 -0
- package/dist/shared/jwt-plugin/is-retryable-auth-code.d.ts +8 -0
- package/dist/shared/jwt-plugin/is-retryable-auth-code.js +15 -0
- package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.d.ts +14 -0
- package/dist/shared/jwt-plugin/normalize-clock-skew-seconds.js +23 -0
- package/dist/shared/jwt-plugin/normalize-default-expires-in.d.ts +16 -0
- package/dist/shared/jwt-plugin/normalize-default-expires-in.js +36 -0
- package/dist/shared/jwt-plugin/read-custom-claims.d.ts +12 -0
- package/dist/shared/jwt-plugin/read-custom-claims.js +21 -0
- package/dist/shared/jwt-plugin/read-expires-in.d.ts +12 -0
- package/dist/shared/jwt-plugin/read-expires-in.js +20 -0
- package/dist/shared/jwt-plugin/read-session-id.d.ts +11 -0
- package/dist/shared/jwt-plugin/read-session-id.js +17 -0
- package/dist/shared/jwt-plugin/resolve-expires-in.d.ts +14 -0
- package/dist/shared/jwt-plugin/resolve-expires-in.js +31 -0
- package/dist/shared/jwt-plugin/to-date-from-unix-seconds.d.ts +7 -0
- package/dist/shared/jwt-plugin/to-date-from-unix-seconds.js +12 -0
- package/package.json +12 -11
- /package/dist/application/dtos/{type/user-role.type.js → request/change-password.request.js} +0 -0
- /package/dist/application/dtos/{type → types}/index.d.ts +0 -0
- /package/dist/application/dtos/{type → types}/index.js +0 -0
- /package/dist/application/dtos/{type → types}/user-role.type.d.ts +0 -0
|
@@ -1,87 +1,340 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.TokenSessionService = void 0;
|
|
4
|
-
// src/infrastructure/services/token-session.service.ts
|
|
5
4
|
const domain_1 = require("../../domain");
|
|
6
5
|
/**
|
|
7
|
-
*
|
|
6
|
+
* Extrae el `sid` (sessionId) del payload de forma segura.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* -
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* Nota:
|
|
9
|
+
* - En la mayoría de implementaciones, `sid` viene como claim plano.
|
|
10
|
+
* - Si tu `ITokenServicePort` lo entrega de otra forma, ajusta aquí (único lugar).
|
|
11
|
+
*
|
|
12
|
+
* Ventaja:
|
|
13
|
+
* - Centraliza la dependencia en el formato del payload (single source of truth).
|
|
14
|
+
*
|
|
15
|
+
* @param payload Payload verificado del JWT.
|
|
16
|
+
* @returns sessionId (sid).
|
|
17
|
+
*/
|
|
18
|
+
function getSessionIdFromPayload(payload) {
|
|
19
|
+
// Aquí se asume que IJWTPayload expone "sid" como string.
|
|
20
|
+
// Si llegara a ser opcional en el tipo, este método debería retornar string | undefined
|
|
21
|
+
// y el resto del flujo ya está preparado para manejar "falsy".
|
|
22
|
+
return payload.sid;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Servicio de sesiones de usuario (rotación de refresh token) con soporte multi-dispositivo.
|
|
26
|
+
*
|
|
27
|
+
* Capa:
|
|
28
|
+
* - Infraestructura (orquesta ports y persistencia, no contiene reglas de negocio de Auth)
|
|
29
|
+
*
|
|
30
|
+
* Responsabilidades:
|
|
31
|
+
* - Generar/verificar tokens usando ITokenServicePort (plugin: jose / etc.)
|
|
32
|
+
* - Persistir credenciales por sessionId (1 fila = 1 sesión/dispositivo)
|
|
33
|
+
* - Recuperar usuario desde IUserRepositoryPort
|
|
34
|
+
*
|
|
35
|
+
* Reglas clave (multi-dispositivo):
|
|
36
|
+
* - createSession: crea sessionId nuevo (nuevo dispositivo/sesión)
|
|
37
|
+
* - refreshSession: rota tokens manteniendo el MISMO sessionId (misma sesión)
|
|
38
|
+
* - validateSession: valida JWT + valida que la sesión exista (revocable) y no expirada
|
|
14
39
|
*/
|
|
15
40
|
class TokenSessionService {
|
|
16
|
-
constructor(
|
|
17
|
-
|
|
18
|
-
|
|
41
|
+
constructor(
|
|
42
|
+
/**
|
|
43
|
+
* Puerto para operaciones JWT (generar/verificar/expiración).
|
|
44
|
+
* Implementación típica: jose (via @jmlq/auth-plugin-jose).
|
|
45
|
+
*/
|
|
46
|
+
tokenService,
|
|
47
|
+
/**
|
|
48
|
+
* Puerto de usuarios (fuente de verdad del usuario).
|
|
49
|
+
* Se usa para:
|
|
50
|
+
* - encontrar usuario por id (payload.sub)
|
|
51
|
+
* - validar canLogin()
|
|
52
|
+
*/
|
|
53
|
+
userRepository,
|
|
54
|
+
/**
|
|
55
|
+
* Puerto de credenciales/sesiones (persistencia revocable).
|
|
56
|
+
* Se usa para:
|
|
57
|
+
* - save / findBySessionId / findByRefreshToken
|
|
58
|
+
* - deleteByRefreshToken / deleteBySessionId
|
|
59
|
+
*/
|
|
60
|
+
credentialRepository,
|
|
61
|
+
/**
|
|
62
|
+
* Expiración del access token en formato humano.
|
|
63
|
+
* @example "15m"
|
|
64
|
+
*
|
|
65
|
+
* Nota:
|
|
66
|
+
* - La interpretación la hace el tokenService (o un parser interno).
|
|
67
|
+
*/
|
|
68
|
+
accessTokenExpiration = "15m",
|
|
69
|
+
/**
|
|
70
|
+
* Expiración del refresh token en formato humano.
|
|
71
|
+
* @example "7d"
|
|
72
|
+
*/
|
|
73
|
+
refreshTokenExpiration = "7d") {
|
|
19
74
|
this.tokenService = tokenService;
|
|
20
75
|
this.userRepository = userRepository;
|
|
21
76
|
this.credentialRepository = credentialRepository;
|
|
22
77
|
this.accessTokenExpiration = accessTokenExpiration;
|
|
23
78
|
this.refreshTokenExpiration = refreshTokenExpiration;
|
|
24
79
|
}
|
|
25
|
-
|
|
26
|
-
|
|
80
|
+
/**
|
|
81
|
+
* Emite access/refresh tokens para un usuario y una sesión dada.
|
|
82
|
+
*
|
|
83
|
+
* Este método es el “núcleo” de emisión de tokens y su claim policy.
|
|
84
|
+
*
|
|
85
|
+
* Qué hace:
|
|
86
|
+
* 1) Construye claims del usuario (id/email/roles)
|
|
87
|
+
* 2) Deriva permissions efectivas desde roles (RBAC)
|
|
88
|
+
* 3) Construye customClaims.permissions para que el API pueda autorizar (requirePermissions)
|
|
89
|
+
* 4) Genera access + refresh
|
|
90
|
+
* 5) Obtiene expiresAt desde el accessToken (fuente robusta: tokenService)
|
|
91
|
+
*
|
|
92
|
+
* Importante:
|
|
93
|
+
* - permissions vienen de roles.getValuePublic().permissions
|
|
94
|
+
* (en el host se “pegan” desde AccessSnapshotResolver)
|
|
95
|
+
*
|
|
96
|
+
* @param user Usuario autenticado.
|
|
97
|
+
* @param sessionId Identificador de sesión/dispositivo.
|
|
98
|
+
*/
|
|
99
|
+
async issueTokens(user, sessionId) {
|
|
100
|
+
/**
|
|
101
|
+
* Política:
|
|
102
|
+
* - JWT delgado: NO roles, NO permissions.
|
|
103
|
+
* - Authorization se resuelve en el host (BDD) por middleware/servicio.
|
|
104
|
+
*/
|
|
105
|
+
const userClaims = {
|
|
106
|
+
id: user.id.getValue(),
|
|
107
|
+
// email/roles son opcionales y NO se incluyen por defecto.
|
|
108
|
+
};
|
|
109
|
+
// Custom claims: no forzamos nada desde el core.
|
|
110
|
+
// Si el host quiere customClaims, debe hacerlo en su propio flujo/política.
|
|
111
|
+
const customClaims = {};
|
|
112
|
+
/**
|
|
113
|
+
* Generar access token:
|
|
114
|
+
* - sessionId se coloca en "sid"
|
|
115
|
+
* - expiresIn aplica política de expiración configurada
|
|
116
|
+
* - customClaims adjunta permissions para autorización en el API
|
|
117
|
+
*/
|
|
27
118
|
const accessToken = await this.tokenService.generateAccessToken({
|
|
28
|
-
user:
|
|
29
|
-
id: user.id.toString(),
|
|
30
|
-
email: user.email.toString(),
|
|
31
|
-
roles: user.roles.map((r) => r.getValuePublic()),
|
|
32
|
-
},
|
|
119
|
+
user: userClaims,
|
|
33
120
|
expiresIn: this.accessTokenExpiration,
|
|
121
|
+
sessionId: sessionId.getValue(),
|
|
122
|
+
customClaims,
|
|
34
123
|
});
|
|
124
|
+
/**
|
|
125
|
+
* Generar refresh token:
|
|
126
|
+
* - Mantiene mismo sid
|
|
127
|
+
* - Suele tener expiración mayor
|
|
128
|
+
* - También transporta customClaims (depende de tu política; aquí se incluye por consistencia)
|
|
129
|
+
*/
|
|
35
130
|
const refreshToken = await this.tokenService.generateRefreshToken({
|
|
36
|
-
user:
|
|
37
|
-
id: user.id.toString(),
|
|
38
|
-
email: user.email.toString(),
|
|
39
|
-
roles: user.roles.map((r) => r.getValuePublic()),
|
|
40
|
-
},
|
|
131
|
+
user: userClaims,
|
|
41
132
|
expiresIn: this.refreshTokenExpiration,
|
|
133
|
+
sessionId: sessionId.getValue(),
|
|
134
|
+
customClaims,
|
|
42
135
|
});
|
|
43
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Expiración del access token:
|
|
138
|
+
* - Se deriva desde el token (tokenService), evitando parseos propios
|
|
139
|
+
* - Reduce riesgo de inconsistencias entre "exp" y cálculos internos
|
|
140
|
+
*/
|
|
44
141
|
const expiresAt = await this.tokenService.getTokenExpiration(accessToken);
|
|
45
|
-
|
|
142
|
+
return { accessToken, refreshToken, expiresAt };
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Crea una nueva sesión (nuevo dispositivo).
|
|
146
|
+
*
|
|
147
|
+
* Flujo:
|
|
148
|
+
* 1) Genera un sessionId nuevo
|
|
149
|
+
* 2) Emite tokens atados a ese sessionId
|
|
150
|
+
* 3) Construye Credential (entidad de dominio de sesión)
|
|
151
|
+
* 4) Persiste credencial
|
|
152
|
+
*
|
|
153
|
+
* Efecto:
|
|
154
|
+
* - multi-dispositivo: un usuario puede tener N credenciales activas (N sessionId)
|
|
155
|
+
*
|
|
156
|
+
* @param user Usuario autenticado.
|
|
157
|
+
* @returns Credencial persistida para la nueva sesión.
|
|
158
|
+
*/
|
|
159
|
+
async createSession(user) {
|
|
160
|
+
const sessionId = domain_1.Id.generate();
|
|
161
|
+
const { accessToken, refreshToken, expiresAt } = await this.issueTokens(user, sessionId);
|
|
162
|
+
/**
|
|
163
|
+
* Credential representa la sesión activa.
|
|
164
|
+
* Suele incluir:
|
|
165
|
+
* - sessionId
|
|
166
|
+
* - userId
|
|
167
|
+
* - accessToken actual
|
|
168
|
+
* - refreshToken actual
|
|
169
|
+
* - expiresAt (del access)
|
|
170
|
+
*/
|
|
171
|
+
const credential = domain_1.Credential.create(sessionId, user.id, accessToken, refreshToken, expiresAt);
|
|
46
172
|
await this.credentialRepository.save(credential);
|
|
47
173
|
return credential;
|
|
48
174
|
}
|
|
49
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Rota refresh token manteniendo el MISMO sessionId (misma sesión/dispositivo).
|
|
177
|
+
*
|
|
178
|
+
* Flujo:
|
|
179
|
+
* 1) Busca credencial por refreshToken (revocable)
|
|
180
|
+
* 2) Verifica refreshToken (firma/exp)
|
|
181
|
+
* 3) Recupera usuario (payload.sub) y valida canLogin()
|
|
182
|
+
* 4) Re-emite tokens con el MISMO sessionId
|
|
183
|
+
* 5) Reemplaza credencial persistida (delete + save)
|
|
184
|
+
*
|
|
185
|
+
* Seguridad:
|
|
186
|
+
* - Si el refreshToken no existe en repositorio => sesión revocada o token inválido => TokenExpiredError
|
|
187
|
+
* - Verificación JWT también puede lanzar => TokenExpiredError
|
|
188
|
+
*
|
|
189
|
+
* @param refreshToken Refresh token actual.
|
|
190
|
+
* @returns Nueva credencial rotada para la misma sesión.
|
|
191
|
+
*/
|
|
50
192
|
async refreshSession(refreshToken) {
|
|
51
193
|
const existing = await this.credentialRepository.findByRefreshToken(refreshToken);
|
|
194
|
+
// Si no existe el refreshToken en DB, se considera expirado/revocado
|
|
52
195
|
if (!existing)
|
|
53
|
-
throw new domain_1.
|
|
54
|
-
|
|
196
|
+
throw new domain_1.TokenExpiredError();
|
|
197
|
+
/**
|
|
198
|
+
* Verificación criptográfica y claims:
|
|
199
|
+
* - Si la firma o exp fallan => TokenExpiredError
|
|
200
|
+
*
|
|
201
|
+
* Nota:
|
|
202
|
+
* - Se oculta el error real por seguridad (no filtrar detalles al cliente).
|
|
203
|
+
*/
|
|
55
204
|
const payload = await this.tokenService
|
|
56
205
|
.verifyRefreshToken(refreshToken)
|
|
57
206
|
.catch(() => {
|
|
58
|
-
throw new domain_1.
|
|
207
|
+
throw new domain_1.TokenExpiredError();
|
|
59
208
|
});
|
|
209
|
+
/**
|
|
210
|
+
* El sub del payload identifica al usuario.
|
|
211
|
+
* Se re-carga para:
|
|
212
|
+
* - validar existencia
|
|
213
|
+
* - validar estado (canLogin)
|
|
214
|
+
* - obtener roles/permisos actuales (si el repo resuelve RBAC en cada fetch)
|
|
215
|
+
*/
|
|
60
216
|
const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
|
|
61
217
|
if (!user)
|
|
62
218
|
throw new domain_1.UserNotFoundError();
|
|
63
219
|
if (!user.canLogin())
|
|
64
220
|
throw new domain_1.UserDisabledError();
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
221
|
+
/**
|
|
222
|
+
* Mantener identidad de sesión:
|
|
223
|
+
* - NO se crea un sessionId nuevo
|
|
224
|
+
* - Se rota token, pero el dispositivo/sesión sigue siendo el mismo
|
|
225
|
+
*/
|
|
226
|
+
const sessionId = existing.sessionId;
|
|
227
|
+
const { accessToken, refreshToken: newRefreshToken, expiresAt, } = await this.issueTokens(user, sessionId);
|
|
228
|
+
const rotated = domain_1.Credential.create(sessionId, user.id, accessToken, newRefreshToken, expiresAt);
|
|
229
|
+
/**
|
|
230
|
+
* Rotación atómica (single-use):
|
|
231
|
+
* - Si otro request ya consumió este refreshToken, esta operación debe fallar.
|
|
232
|
+
* - Evita que el mismo refreshToken se use dos veces bajo concurrencia.
|
|
233
|
+
*/
|
|
234
|
+
const rotatedOk = await this.credentialRepository.rotateByRefreshToken(refreshToken, rotated);
|
|
235
|
+
if (!rotatedOk) {
|
|
236
|
+
// Token ya consumido/revocado/no existe (no filtramos detalle)
|
|
237
|
+
throw new domain_1.TokenExpiredError();
|
|
238
|
+
}
|
|
239
|
+
return rotated;
|
|
70
240
|
}
|
|
71
|
-
|
|
241
|
+
/**
|
|
242
|
+
* Valida una sesión a partir de access token.
|
|
243
|
+
*
|
|
244
|
+
* Este método soporta “revocación” (token válido criptográficamente pero sesión eliminada en DB).
|
|
245
|
+
*
|
|
246
|
+
* Reglas:
|
|
247
|
+
* - JWT debe ser válido (firma/exp/issuer/aud según plugin)
|
|
248
|
+
* - Debe existir `sid` en el payload
|
|
249
|
+
* - La sesión (sid) debe existir en el repositorio
|
|
250
|
+
* - (recomendado) accessToken debe coincidir con el almacenado (solo el último token es válido)
|
|
251
|
+
* - La credencial no debe estar expirada (según su estado interno)
|
|
252
|
+
* - El usuario debe existir y poder loguearse
|
|
253
|
+
*
|
|
254
|
+
* @param accessToken Access token recibido.
|
|
255
|
+
* @returns Usuario si sesión es válida, o null si no lo es.
|
|
256
|
+
*/
|
|
72
257
|
async validateSession(accessToken) {
|
|
73
258
|
try {
|
|
259
|
+
/**
|
|
260
|
+
* verifyAccessToken:
|
|
261
|
+
* - valida firma y exp y claims críticos según configuración del tokenService
|
|
262
|
+
* - retorna payload tipado como IJWTPayload
|
|
263
|
+
*/
|
|
74
264
|
const payload = await this.tokenService.verifyAccessToken(accessToken);
|
|
265
|
+
/**
|
|
266
|
+
* sid:
|
|
267
|
+
* - identifica la sesión/dispositivo
|
|
268
|
+
* - si no existe, no podemos validar sesión revocable => null
|
|
269
|
+
*/
|
|
270
|
+
const sid = getSessionIdFromPayload(payload);
|
|
271
|
+
if (!sid)
|
|
272
|
+
return null;
|
|
273
|
+
/**
|
|
274
|
+
* Validación de sesión en storage:
|
|
275
|
+
* - si no existe => sesión revocada => null
|
|
276
|
+
*/
|
|
277
|
+
const credential = await this.credentialRepository.findBySessionId(new domain_1.Id(sid));
|
|
278
|
+
if (!credential)
|
|
279
|
+
return null;
|
|
280
|
+
/**
|
|
281
|
+
* Validación recomendada:
|
|
282
|
+
* - asegura que solo el último accessToken emitido para esa sesión sea aceptado
|
|
283
|
+
* - previene replay de accessTokens antiguos dentro de la misma sesión
|
|
284
|
+
*
|
|
285
|
+
* Nota:
|
|
286
|
+
* - Esto exige que el repo persista accessToken actual.
|
|
287
|
+
*/
|
|
288
|
+
if (credential.accessToken !== accessToken)
|
|
289
|
+
return null;
|
|
290
|
+
/**
|
|
291
|
+
* Validación de expiración a nivel de credencial:
|
|
292
|
+
* - respaldo adicional (además de "exp" del JWT)
|
|
293
|
+
* - útil si tu sistema modela expiración o revocación interna
|
|
294
|
+
*/
|
|
295
|
+
if (credential.isExpired())
|
|
296
|
+
return null;
|
|
297
|
+
/**
|
|
298
|
+
* sub del payload identifica al usuario.
|
|
299
|
+
* Se valida existencia y estado.
|
|
300
|
+
*/
|
|
75
301
|
const user = await this.userRepository.findById(new domain_1.Id(payload.sub));
|
|
76
302
|
return user && user.canLogin() ? user : null;
|
|
77
303
|
}
|
|
78
|
-
catch {
|
|
79
|
-
|
|
304
|
+
catch (e) {
|
|
305
|
+
/**
|
|
306
|
+
* SessionAuthError:
|
|
307
|
+
* - encapsula el error interno (logging/observabilidad)
|
|
308
|
+
* - evita filtrar detalles sensibles al exterior
|
|
309
|
+
*/
|
|
310
|
+
throw new domain_1.SessionAuthError("Session Authentication failed", e);
|
|
80
311
|
}
|
|
81
312
|
}
|
|
82
|
-
|
|
313
|
+
/**
|
|
314
|
+
* Revoca una sesión usando refresh token.
|
|
315
|
+
*
|
|
316
|
+
* Uso típico:
|
|
317
|
+
* - logout donde el cliente envía refreshToken (cookie/body)
|
|
318
|
+
*
|
|
319
|
+
* Efecto:
|
|
320
|
+
* - la sesión deja de poder refrescar tokens
|
|
321
|
+
* - si validateSession compara accessToken, también invalida el access actual cuando se borra la credencial
|
|
322
|
+
*
|
|
323
|
+
* @param refreshToken Refresh token actual.
|
|
324
|
+
*/
|
|
83
325
|
async revokeSession(refreshToken) {
|
|
84
326
|
await this.credentialRepository.deleteByRefreshToken(refreshToken);
|
|
85
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Revoca una sesión por sessionId (logout por dispositivo).
|
|
330
|
+
*
|
|
331
|
+
* Uso típico:
|
|
332
|
+
* - logout selectivo (cerrar solo este dispositivo)
|
|
333
|
+
*
|
|
334
|
+
* @param sessionId Identificador de sesión/dispositivo.
|
|
335
|
+
*/
|
|
336
|
+
async revokeSessionById(sessionId) {
|
|
337
|
+
await this.credentialRepository.deleteBySessionId(sessionId);
|
|
338
|
+
}
|
|
86
339
|
}
|
|
87
340
|
exports.TokenSessionService = TokenSessionService;
|
|
@@ -1,5 +1,10 @@
|
|
|
1
|
-
import { ICredentialRepositoryPort, IPasswordHasherPort, IPasswordPolicyPort, ITokenServicePort, ITokenSessionPort, IUserRepositoryPort } from "../../domain/ports";
|
|
2
|
-
import { LoginWithPasswordUseCase, LogoutUseCase, RefreshTokenUseCase, RegisterUserUseCase } from "../../application/use-cases";
|
|
1
|
+
import { ICredentialRepositoryPort, IPasswordHasherPort, IPasswordPolicyPort, ITokenServicePort, ITokenSessionPort, IUserRepositoryPort, IPasswordResetTokenPort, IEmailVerificationTokenPort } from "../../domain/ports";
|
|
2
|
+
import { ChangePasswordUseCase, LoginWithPasswordUseCase, LogoutUseCase, MeUseCase, RefreshTokenUseCase, RegisterUserUseCase, RequestPasswordResetUseCase, ResetPasswordUseCase, VerifyEmailUseCase } from "../../application/use-cases";
|
|
3
|
+
/**
|
|
4
|
+
* IAuthServiceContainer es el punto de composición del módulo de autenticación
|
|
5
|
+
* Es un contrato que agrupa todas las dependencias y casos de uso de Auth ya construidos,
|
|
6
|
+
* listos para ser consumidos por una aplicación (API REST, GraphQL, CLI, etc.).
|
|
7
|
+
*/
|
|
3
8
|
export interface IAuthServiceContainer {
|
|
4
9
|
userRepository: IUserRepositoryPort;
|
|
5
10
|
credentialRepository: ICredentialRepositoryPort;
|
|
@@ -7,8 +12,22 @@ export interface IAuthServiceContainer {
|
|
|
7
12
|
tokenService: ITokenServicePort;
|
|
8
13
|
passwordPolicy: IPasswordPolicyPort;
|
|
9
14
|
tokenSession: ITokenSessionPort;
|
|
15
|
+
passwordResetToken: IPasswordResetTokenPort;
|
|
16
|
+
/**
|
|
17
|
+
* Puerto para la gestión de tokens de verificación de email.
|
|
18
|
+
*
|
|
19
|
+
* Responsabilidad:
|
|
20
|
+
* - Emitir tokens temporales (single-use) asociados a un usuario.
|
|
21
|
+
* - Validar y consumir el token cuando el usuario accede al enlace de verificación.
|
|
22
|
+
*/
|
|
23
|
+
emailVerificationToken: IEmailVerificationTokenPort;
|
|
10
24
|
registerUserUseCase: RegisterUserUseCase;
|
|
11
25
|
loginWithPasswordUseCase: LoginWithPasswordUseCase;
|
|
12
26
|
refreshTokenUseCase: RefreshTokenUseCase;
|
|
13
27
|
logoutUseCase: LogoutUseCase;
|
|
28
|
+
requestPasswordResetUseCase: RequestPasswordResetUseCase;
|
|
29
|
+
resetPasswordUseCase: ResetPasswordUseCase;
|
|
30
|
+
changePasswordUseCase: ChangePasswordUseCase;
|
|
31
|
+
verifyEmailUseCase: VerifyEmailUseCase;
|
|
32
|
+
meUseCase: MeUseCase;
|
|
14
33
|
}
|
package/dist/shared/index.d.ts
CHANGED
package/dist/shared/index.js
CHANGED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createJwtId = createJwtId;
|
|
4
|
+
/**
|
|
5
|
+
* Obtiene el tiempo actual en segundos Unix.
|
|
6
|
+
*
|
|
7
|
+
* @returns Timestamp Unix (segundos).
|
|
8
|
+
*/
|
|
9
|
+
function nowUnixSeconds() {
|
|
10
|
+
return Math.floor(Date.now() / 1000);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Genera un identificador único para el claim `jti`.
|
|
14
|
+
* - Usa crypto.randomUUID cuando está disponible.
|
|
15
|
+
* - Fallback no-crypto para entornos legacy/dev.
|
|
16
|
+
*/
|
|
17
|
+
function createJwtId() {
|
|
18
|
+
if (hasCryptoRandomUUID(globalThis)) {
|
|
19
|
+
return globalThis.crypto.randomUUID();
|
|
20
|
+
}
|
|
21
|
+
return `jti_${nowUnixSeconds()}_${Math.random().toString(16).slice(2)}`;
|
|
22
|
+
}
|
|
23
|
+
function hasCryptoRandomUUID(value) {
|
|
24
|
+
return (typeof value === "object" &&
|
|
25
|
+
value !== null &&
|
|
26
|
+
"crypto" in value &&
|
|
27
|
+
typeof value.crypto === "object" &&
|
|
28
|
+
typeof value.crypto
|
|
29
|
+
?.randomUUID === "function");
|
|
30
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export * from "./normalize-clock-skew-seconds";
|
|
2
|
+
export * from "./normalize-default-expires-in";
|
|
3
|
+
export * from "./read-expires-in";
|
|
4
|
+
export * from "./read-custom-claims";
|
|
5
|
+
export * from "./resolve-expires-in";
|
|
6
|
+
export * from "./is-retryable-auth-code";
|
|
7
|
+
export * from "./read-session-id";
|
|
8
|
+
export * from "./to-date-from-unix-seconds";
|
|
9
|
+
export * from "./create-jwt-id";
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./normalize-clock-skew-seconds"), exports);
|
|
18
|
+
__exportStar(require("./normalize-default-expires-in"), exports);
|
|
19
|
+
__exportStar(require("./read-expires-in"), exports);
|
|
20
|
+
__exportStar(require("./read-custom-claims"), exports);
|
|
21
|
+
__exportStar(require("./resolve-expires-in"), exports);
|
|
22
|
+
__exportStar(require("./is-retryable-auth-code"), exports);
|
|
23
|
+
__exportStar(require("./read-session-id"), exports);
|
|
24
|
+
__exportStar(require("./to-date-from-unix-seconds"), exports);
|
|
25
|
+
__exportStar(require("./create-jwt-id"), exports);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { AuthErrorCode } from "../../domain/errors";
|
|
2
|
+
/**
|
|
3
|
+
* Determina si un error del core (por código) es "retryable".
|
|
4
|
+
*
|
|
5
|
+
* Responsabilidad única:
|
|
6
|
+
* - Decidir reintento SOLO por `code` (sin jose, sin message, sin heurísticas).
|
|
7
|
+
*/
|
|
8
|
+
export declare function isRetryableAuthCode(code: AuthErrorCode): boolean;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.isRetryableAuthCode = isRetryableAuthCode;
|
|
4
|
+
/**
|
|
5
|
+
* Determina si un error del core (por código) es "retryable".
|
|
6
|
+
*
|
|
7
|
+
* Responsabilidad única:
|
|
8
|
+
* - Decidir reintento SOLO por `code` (sin jose, sin message, sin heurísticas).
|
|
9
|
+
*/
|
|
10
|
+
function isRetryableAuthCode(code) {
|
|
11
|
+
return (code === "SIGNATURE_INVALID" ||
|
|
12
|
+
code === "ALGORITHM_UNSUPPORTED" ||
|
|
13
|
+
code === "KEY_MISMATCH" ||
|
|
14
|
+
code === "KEY_NOT_FOUND");
|
|
15
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normaliza clockSkewSeconds (en segundos).
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidad única:
|
|
5
|
+
* - Aceptar únicamente números válidos
|
|
6
|
+
* - Convertir a entero (floor)
|
|
7
|
+
* - Asegurar >= 0
|
|
8
|
+
*
|
|
9
|
+
* Reglas:
|
|
10
|
+
* - no number / NaN => undefined
|
|
11
|
+
* - < 0 => 0
|
|
12
|
+
* - >== 0 => floor(value)
|
|
13
|
+
*/
|
|
14
|
+
export declare function normalizeClockSkewSeconds(value: number | undefined): number | undefined;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeClockSkewSeconds = normalizeClockSkewSeconds;
|
|
4
|
+
/**
|
|
5
|
+
* Normaliza clockSkewSeconds (en segundos).
|
|
6
|
+
*
|
|
7
|
+
* Responsabilidad única:
|
|
8
|
+
* - Aceptar únicamente números válidos
|
|
9
|
+
* - Convertir a entero (floor)
|
|
10
|
+
* - Asegurar >= 0
|
|
11
|
+
*
|
|
12
|
+
* Reglas:
|
|
13
|
+
* - no number / NaN => undefined
|
|
14
|
+
* - < 0 => 0
|
|
15
|
+
* - >== 0 => floor(value)
|
|
16
|
+
*/
|
|
17
|
+
function normalizeClockSkewSeconds(value) {
|
|
18
|
+
if (typeof value !== "number" || Number.isNaN(value))
|
|
19
|
+
return undefined;
|
|
20
|
+
if (value < 0)
|
|
21
|
+
return 0;
|
|
22
|
+
return Math.floor(value);
|
|
23
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normaliza defaults de expiración usados por plugins JWT.
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidad única:
|
|
5
|
+
* - Aceptar un shape compatible con { accessToken?, refreshToken? }
|
|
6
|
+
* - Trim de strings
|
|
7
|
+
* - Vacío => omitido
|
|
8
|
+
* - Si queda vacío => undefined
|
|
9
|
+
*
|
|
10
|
+
* Importante:
|
|
11
|
+
* - No depende de types de plugins para evitar acoplamiento.
|
|
12
|
+
*/
|
|
13
|
+
export declare function normalizeDefaultExpiresIn<T extends {
|
|
14
|
+
accessToken?: string;
|
|
15
|
+
refreshToken?: string;
|
|
16
|
+
}>(value: T | undefined): T | undefined;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeDefaultExpiresIn = normalizeDefaultExpiresIn;
|
|
4
|
+
/**
|
|
5
|
+
* Normaliza defaults de expiración usados por plugins JWT.
|
|
6
|
+
*
|
|
7
|
+
* Responsabilidad única:
|
|
8
|
+
* - Aceptar un shape compatible con { accessToken?, refreshToken? }
|
|
9
|
+
* - Trim de strings
|
|
10
|
+
* - Vacío => omitido
|
|
11
|
+
* - Si queda vacío => undefined
|
|
12
|
+
*
|
|
13
|
+
* Importante:
|
|
14
|
+
* - No depende de types de plugins para evitar acoplamiento.
|
|
15
|
+
*/
|
|
16
|
+
function normalizeDefaultExpiresIn(value) {
|
|
17
|
+
if (!value)
|
|
18
|
+
return undefined;
|
|
19
|
+
const out = {};
|
|
20
|
+
const accessToken = normalizeOptionalNonEmptyString(value.accessToken);
|
|
21
|
+
if (accessToken)
|
|
22
|
+
out.accessToken = accessToken;
|
|
23
|
+
const refreshToken = normalizeOptionalNonEmptyString(value.refreshToken);
|
|
24
|
+
if (refreshToken)
|
|
25
|
+
out.refreshToken = refreshToken;
|
|
26
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Helper local (mínimo) para evitar dependencia circular en exports del core.
|
|
30
|
+
*/
|
|
31
|
+
function normalizeOptionalNonEmptyString(value) {
|
|
32
|
+
if (typeof value !== "string")
|
|
33
|
+
return undefined;
|
|
34
|
+
const v = value.trim();
|
|
35
|
+
return v.length > 0 ? v : undefined;
|
|
36
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lee `customClaims` desde unknown.
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidad única:
|
|
5
|
+
* - Aceptar únicamente un objeto plano serializable (Record<string, unknown>)
|
|
6
|
+
*
|
|
7
|
+
* Reglas:
|
|
8
|
+
* - undefined/null => undefined
|
|
9
|
+
* - arrays => undefined
|
|
10
|
+
* - objetos => Record<string, unknown>
|
|
11
|
+
*/
|
|
12
|
+
export declare function readCustomClaims(value: unknown): Record<string, unknown> | undefined;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readCustomClaims = readCustomClaims;
|
|
4
|
+
/**
|
|
5
|
+
* Lee `customClaims` desde unknown.
|
|
6
|
+
*
|
|
7
|
+
* Responsabilidad única:
|
|
8
|
+
* - Aceptar únicamente un objeto plano serializable (Record<string, unknown>)
|
|
9
|
+
*
|
|
10
|
+
* Reglas:
|
|
11
|
+
* - undefined/null => undefined
|
|
12
|
+
* - arrays => undefined
|
|
13
|
+
* - objetos => Record<string, unknown>
|
|
14
|
+
*/
|
|
15
|
+
function readCustomClaims(value) {
|
|
16
|
+
if (!value || typeof value !== "object")
|
|
17
|
+
return undefined;
|
|
18
|
+
if (Array.isArray(value))
|
|
19
|
+
return undefined;
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lee `expiresIn` desde unknown.
|
|
3
|
+
*
|
|
4
|
+
* Responsabilidad única:
|
|
5
|
+
* - Normalizar un valor desconocido a `string | undefined`
|
|
6
|
+
*
|
|
7
|
+
* Reglas:
|
|
8
|
+
* - no string => undefined
|
|
9
|
+
* - string vacío => undefined
|
|
10
|
+
* - string con espacios => trim()
|
|
11
|
+
*/
|
|
12
|
+
export declare function readExpiresIn(value: unknown): string | undefined;
|