@aranzatech/aranza-auth 0.2.1 → 0.2.3
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/CHANGELOG.md +67 -2
- package/README.md +180 -16
- package/SECURITY.md +11 -0
- package/dist/{auth-repository.interface-9PpDVOs8.d.cts → auth-repository.interface--1rv0RCD.d.cts} +22 -3
- package/dist/{auth-repository.interface-9PpDVOs8.d.ts → auth-repository.interface--1rv0RCD.d.ts} +22 -3
- package/dist/index.cjs +621 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +142 -11
- package/dist/index.d.ts +142 -11
- package/dist/index.js +617 -188
- package/dist/index.js.map +1 -1
- package/dist/mongo/index.cjs +34 -6
- package/dist/mongo/index.cjs.map +1 -1
- package/dist/mongo/index.d.cts +4 -1
- package/dist/mongo/index.d.ts +4 -1
- package/dist/mongo/index.js +34 -6
- package/dist/mongo/index.js.map +1 -1
- package/package.json +12 -1
package/dist/index.cjs
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var swagger = require('@nestjs/swagger');
|
|
4
|
+
var classValidator = require('class-validator');
|
|
3
5
|
var common = require('@nestjs/common');
|
|
4
|
-
var core = require('@nestjs/core');
|
|
5
6
|
var jwt = require('@nestjs/jwt');
|
|
6
7
|
var passport = require('@nestjs/passport');
|
|
7
|
-
var
|
|
8
|
+
var bcrypt = require('bcryptjs');
|
|
8
9
|
var crypto = require('crypto');
|
|
9
10
|
var passportJwt = require('passport-jwt');
|
|
10
|
-
var
|
|
11
|
+
var core = require('@nestjs/core');
|
|
11
12
|
|
|
12
13
|
function _interopNamespace(e) {
|
|
13
14
|
if (e && e.__esModule) return e;
|
|
@@ -27,7 +28,7 @@ function _interopNamespace(e) {
|
|
|
27
28
|
return Object.freeze(n);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
var
|
|
31
|
+
var bcrypt__namespace = /*#__PURE__*/_interopNamespace(bcrypt);
|
|
31
32
|
|
|
32
33
|
var __defProp = Object.defineProperty;
|
|
33
34
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -51,18 +52,258 @@ var AUTH_RATE_LIMIT_PRESETS = {
|
|
|
51
52
|
passwordReset: { name: "auth-password-reset", ttl: 6e4, limit: 3 }
|
|
52
53
|
};
|
|
53
54
|
|
|
55
|
+
// src/constants/rate-limit.routes.ts
|
|
56
|
+
var AUTH_RATE_LIMIT_ROUTES = {
|
|
57
|
+
login: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
58
|
+
register: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
59
|
+
refresh: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
60
|
+
"forgot-password": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
61
|
+
"reset-password": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
62
|
+
"resend-verification": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
63
|
+
default: AUTH_RATE_LIMIT_PRESETS.default
|
|
64
|
+
};
|
|
65
|
+
|
|
54
66
|
// src/constants/auth-errors.ts
|
|
55
67
|
var AuthErrorCode = {
|
|
56
|
-
INVALID_CREDENTIALS: "
|
|
57
|
-
INVALID_REFRESH_TOKEN: "
|
|
68
|
+
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
69
|
+
INVALID_REFRESH_TOKEN: "INVALID_REFRESH_TOKEN",
|
|
58
70
|
REFRESH_TOKEN_REUSE: "REFRESH_TOKEN_REUSE",
|
|
59
71
|
ACCOUNT_DISABLED: "ACCOUNT_DISABLED",
|
|
72
|
+
ACCOUNT_NOT_FOUND: "ACCOUNT_NOT_FOUND",
|
|
60
73
|
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
61
74
|
TOKEN_INVALID_OR_EXPIRED: "TOKEN_INVALID_OR_EXPIRED",
|
|
62
75
|
ACCOUNT_LOCKED: "ACCOUNT_LOCKED",
|
|
63
76
|
INVALID_CURRENT_PASSWORD: "INVALID_CURRENT_PASSWORD",
|
|
64
|
-
PASSWORD_UNCHANGED: "PASSWORD_UNCHANGED"
|
|
77
|
+
PASSWORD_UNCHANGED: "PASSWORD_UNCHANGED",
|
|
78
|
+
PASSWORD_CHANGED: "PASSWORD_CHANGED",
|
|
79
|
+
/** Missing or invalid Bearer token on a protected route. */
|
|
80
|
+
UNAUTHORIZED: "UNAUTHORIZED"
|
|
81
|
+
};
|
|
82
|
+
var AuthTokensDto = class {
|
|
83
|
+
};
|
|
84
|
+
__decorateClass([
|
|
85
|
+
swagger.ApiProperty({ type: String, example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
|
86
|
+
], AuthTokensDto.prototype, "accessToken", 2);
|
|
87
|
+
__decorateClass([
|
|
88
|
+
swagger.ApiProperty({ type: String, example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
|
89
|
+
], AuthTokensDto.prototype, "refreshToken", 2);
|
|
90
|
+
var ChangePasswordDto = class {
|
|
91
|
+
};
|
|
92
|
+
__decorateClass([
|
|
93
|
+
swagger.ApiProperty({ type: String, example: "CurrentPassword1", minLength: 1, maxLength: 128 }),
|
|
94
|
+
classValidator.IsString(),
|
|
95
|
+
classValidator.IsNotEmpty(),
|
|
96
|
+
classValidator.Length(1, 128)
|
|
97
|
+
], ChangePasswordDto.prototype, "currentPassword", 2);
|
|
98
|
+
__decorateClass([
|
|
99
|
+
swagger.ApiProperty({ type: String, example: "NewPassword1", minLength: 8, maxLength: 128 }),
|
|
100
|
+
classValidator.IsString(),
|
|
101
|
+
classValidator.IsNotEmpty(),
|
|
102
|
+
classValidator.Length(8, 128)
|
|
103
|
+
], ChangePasswordDto.prototype, "newPassword", 2);
|
|
104
|
+
var ForgotPasswordDto = class {
|
|
105
|
+
};
|
|
106
|
+
__decorateClass([
|
|
107
|
+
swagger.ApiProperty({ type: String, example: "user@example.com" }),
|
|
108
|
+
classValidator.IsEmail()
|
|
109
|
+
], ForgotPasswordDto.prototype, "email", 2);
|
|
110
|
+
var LoginDto = class {
|
|
111
|
+
};
|
|
112
|
+
__decorateClass([
|
|
113
|
+
swagger.ApiPropertyOptional({ type: String, example: "user@example.com" }),
|
|
114
|
+
classValidator.IsOptional(),
|
|
115
|
+
classValidator.ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
116
|
+
classValidator.IsEmail(),
|
|
117
|
+
classValidator.Length(3, 255)
|
|
118
|
+
], LoginDto.prototype, "email", 2);
|
|
119
|
+
__decorateClass([
|
|
120
|
+
swagger.ApiPropertyOptional({ type: String, example: "johndoe" }),
|
|
121
|
+
classValidator.IsOptional(),
|
|
122
|
+
classValidator.IsString(),
|
|
123
|
+
classValidator.Length(3, 50)
|
|
124
|
+
], LoginDto.prototype, "username", 2);
|
|
125
|
+
__decorateClass([
|
|
126
|
+
swagger.ApiProperty({ type: String, example: "Password1", minLength: 8, maxLength: 128 }),
|
|
127
|
+
classValidator.IsString(),
|
|
128
|
+
classValidator.IsNotEmpty(),
|
|
129
|
+
classValidator.Length(8, 128)
|
|
130
|
+
], LoginDto.prototype, "password", 2);
|
|
131
|
+
var MeResponseDto = class {
|
|
132
|
+
};
|
|
133
|
+
__decorateClass([
|
|
134
|
+
swagger.ApiProperty({ type: String, example: "507f1f77bcf86cd799439011" })
|
|
135
|
+
], MeResponseDto.prototype, "id", 2);
|
|
136
|
+
__decorateClass([
|
|
137
|
+
swagger.ApiPropertyOptional({ type: String, example: "user@example.com" })
|
|
138
|
+
], MeResponseDto.prototype, "email", 2);
|
|
139
|
+
__decorateClass([
|
|
140
|
+
swagger.ApiPropertyOptional({ type: String, example: "johndoe" })
|
|
141
|
+
], MeResponseDto.prototype, "username", 2);
|
|
142
|
+
__decorateClass([
|
|
143
|
+
swagger.ApiProperty({ type: Boolean, example: true })
|
|
144
|
+
], MeResponseDto.prototype, "emailVerified", 2);
|
|
145
|
+
__decorateClass([
|
|
146
|
+
swagger.ApiProperty({ type: Boolean, example: false })
|
|
147
|
+
], MeResponseDto.prototype, "disabled", 2);
|
|
148
|
+
__decorateClass([
|
|
149
|
+
swagger.ApiPropertyOptional({ type: String, format: "date-time" })
|
|
150
|
+
], MeResponseDto.prototype, "lastLoginAt", 2);
|
|
151
|
+
__decorateClass([
|
|
152
|
+
swagger.ApiPropertyOptional({ type: String, format: "date-time" })
|
|
153
|
+
], MeResponseDto.prototype, "passwordChangedAt", 2);
|
|
154
|
+
var RefreshTokenDto = class {
|
|
155
|
+
};
|
|
156
|
+
__decorateClass([
|
|
157
|
+
swagger.ApiProperty({ type: String, example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }),
|
|
158
|
+
classValidator.IsString(),
|
|
159
|
+
classValidator.IsNotEmpty()
|
|
160
|
+
], RefreshTokenDto.prototype, "refreshToken", 2);
|
|
161
|
+
var RegisterAckDto = class {
|
|
162
|
+
};
|
|
163
|
+
__decorateClass([
|
|
164
|
+
swagger.ApiProperty({ type: Boolean, example: true, enum: [true] })
|
|
165
|
+
], RegisterAckDto.prototype, "registered", 2);
|
|
166
|
+
var RegisterDto = class {
|
|
167
|
+
};
|
|
168
|
+
__decorateClass([
|
|
169
|
+
swagger.ApiPropertyOptional({ type: String, example: "user@example.com" }),
|
|
170
|
+
classValidator.IsOptional(),
|
|
171
|
+
classValidator.ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
172
|
+
classValidator.IsEmail(),
|
|
173
|
+
classValidator.Length(3, 255)
|
|
174
|
+
], RegisterDto.prototype, "email", 2);
|
|
175
|
+
__decorateClass([
|
|
176
|
+
swagger.ApiPropertyOptional({ type: String, example: "johndoe" }),
|
|
177
|
+
classValidator.IsOptional(),
|
|
178
|
+
classValidator.IsString(),
|
|
179
|
+
classValidator.Length(3, 50),
|
|
180
|
+
classValidator.Matches(/^[a-zA-Z0-9._-]+$/)
|
|
181
|
+
], RegisterDto.prototype, "username", 2);
|
|
182
|
+
__decorateClass([
|
|
183
|
+
swagger.ApiProperty({ type: String, example: "Password1", minLength: 8, maxLength: 128 }),
|
|
184
|
+
classValidator.IsString(),
|
|
185
|
+
classValidator.IsNotEmpty(),
|
|
186
|
+
classValidator.Length(8, 128)
|
|
187
|
+
], RegisterDto.prototype, "password", 2);
|
|
188
|
+
var ResendVerificationDto = class {
|
|
189
|
+
};
|
|
190
|
+
__decorateClass([
|
|
191
|
+
swagger.ApiProperty({ type: String, example: "user@example.com" }),
|
|
192
|
+
classValidator.IsEmail()
|
|
193
|
+
], ResendVerificationDto.prototype, "email", 2);
|
|
194
|
+
var ResetPasswordDto = class {
|
|
195
|
+
};
|
|
196
|
+
__decorateClass([
|
|
197
|
+
swagger.ApiProperty({ type: String, example: "reset-token-from-email" }),
|
|
198
|
+
classValidator.IsString(),
|
|
199
|
+
classValidator.IsNotEmpty()
|
|
200
|
+
], ResetPasswordDto.prototype, "token", 2);
|
|
201
|
+
__decorateClass([
|
|
202
|
+
swagger.ApiProperty({ type: String, example: "NewPassword1", minLength: 8, maxLength: 128 }),
|
|
203
|
+
classValidator.IsString(),
|
|
204
|
+
classValidator.IsNotEmpty(),
|
|
205
|
+
classValidator.Length(8, 128)
|
|
206
|
+
], ResetPasswordDto.prototype, "newPassword", 2);
|
|
207
|
+
var VerifyEmailDto = class {
|
|
65
208
|
};
|
|
209
|
+
__decorateClass([
|
|
210
|
+
swagger.ApiProperty({ type: String, example: "verification-token-from-email" }),
|
|
211
|
+
classValidator.IsString(),
|
|
212
|
+
classValidator.IsNotEmpty()
|
|
213
|
+
], VerifyEmailDto.prototype, "token", 2);
|
|
214
|
+
|
|
215
|
+
// src/swagger/setup-swagger.util.ts
|
|
216
|
+
var AUTH_SWAGGER_MODELS = [
|
|
217
|
+
AuthTokensDto,
|
|
218
|
+
ChangePasswordDto,
|
|
219
|
+
ForgotPasswordDto,
|
|
220
|
+
LoginDto,
|
|
221
|
+
MeResponseDto,
|
|
222
|
+
RefreshTokenDto,
|
|
223
|
+
RegisterAckDto,
|
|
224
|
+
RegisterDto,
|
|
225
|
+
ResendVerificationDto,
|
|
226
|
+
ResetPasswordDto,
|
|
227
|
+
VerifyEmailDto
|
|
228
|
+
];
|
|
229
|
+
function describeEnabledFeatures(features) {
|
|
230
|
+
const lines = [];
|
|
231
|
+
if (features.emailVerification === true) {
|
|
232
|
+
lines.push("- Email verification (`POST /auth/verify-email`, `POST /auth/resend-verification`)");
|
|
233
|
+
}
|
|
234
|
+
if (features.passwordReset === true) {
|
|
235
|
+
lines.push("- Password reset (`POST /auth/forgot-password`, `POST /auth/reset-password`)");
|
|
236
|
+
}
|
|
237
|
+
if (features.refreshTokenRotation === false) {
|
|
238
|
+
lines.push("- Refresh token rotation **disabled** (stateless refresh until JWT expiry)");
|
|
239
|
+
}
|
|
240
|
+
if (features.accountLockout === true) {
|
|
241
|
+
lines.push("- Account lockout after failed logins");
|
|
242
|
+
}
|
|
243
|
+
if (lines.length === 0) {
|
|
244
|
+
return "";
|
|
245
|
+
}
|
|
246
|
+
return `
|
|
247
|
+
|
|
248
|
+
## Auth features enabled
|
|
249
|
+
${lines.join("\n")}`;
|
|
250
|
+
}
|
|
251
|
+
function setupAuthSwagger(app, options = {}) {
|
|
252
|
+
const nestApp = app;
|
|
253
|
+
const baseDescription = options.description ?? "REST API with JWT authentication via @aranzatech/aranza-auth";
|
|
254
|
+
const config = new swagger.DocumentBuilder().setTitle(options.title ?? "API").setDescription(
|
|
255
|
+
`${baseDescription}${describeEnabledFeatures(options.features ?? {})}`
|
|
256
|
+
).setVersion(options.version ?? "1.0").addBearerAuth(
|
|
257
|
+
{
|
|
258
|
+
type: "http",
|
|
259
|
+
scheme: "bearer",
|
|
260
|
+
bearerFormat: "JWT",
|
|
261
|
+
description: "Access token from POST /auth/login"
|
|
262
|
+
},
|
|
263
|
+
"access-token"
|
|
264
|
+
).build();
|
|
265
|
+
const document = swagger.SwaggerModule.createDocument(nestApp, config, {
|
|
266
|
+
extraModels: [...AUTH_SWAGGER_MODELS]
|
|
267
|
+
});
|
|
268
|
+
if (options.exportPath != null) {
|
|
269
|
+
void import('fs/promises').then(
|
|
270
|
+
({ writeFile }) => writeFile(options.exportPath, JSON.stringify(document, null, 2), "utf8")
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
swagger.SwaggerModule.setup(options.path ?? "api", nestApp, document);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// src/utils/refresh-token-cookie.util.ts
|
|
277
|
+
var DEFAULT_COOKIE_NAME = "refresh_token";
|
|
278
|
+
var DEFAULT_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
|
|
279
|
+
function resolveCookieOptions(options = {}) {
|
|
280
|
+
return {
|
|
281
|
+
name: options.name ?? DEFAULT_COOKIE_NAME,
|
|
282
|
+
path: options.path ?? "/auth/refresh",
|
|
283
|
+
secure: options.secure ?? true,
|
|
284
|
+
sameSite: options.sameSite ?? "strict",
|
|
285
|
+
maxAgeSeconds: options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS,
|
|
286
|
+
httpOnly: options.httpOnly ?? true
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
function formatCookieAttributes(options) {
|
|
290
|
+
const parts = [
|
|
291
|
+
`Path=${options.path}`,
|
|
292
|
+
`Max-Age=${options.maxAgeSeconds}`,
|
|
293
|
+
`SameSite=${options.sameSite}`
|
|
294
|
+
];
|
|
295
|
+
if (options.secure) parts.push("Secure");
|
|
296
|
+
if (options.httpOnly) parts.push("HttpOnly");
|
|
297
|
+
return parts.join("; ");
|
|
298
|
+
}
|
|
299
|
+
function buildRefreshTokenCookie(refreshToken, options = {}) {
|
|
300
|
+
const resolved = resolveCookieOptions(options);
|
|
301
|
+
return `${resolved.name}=${encodeURIComponent(refreshToken)}; ${formatCookieAttributes(resolved)}`;
|
|
302
|
+
}
|
|
303
|
+
function buildClearRefreshTokenCookie(options = {}) {
|
|
304
|
+
const resolved = resolveCookieOptions(options);
|
|
305
|
+
return `${resolved.name}=; Path=${resolved.path}; Max-Age=0; HttpOnly`;
|
|
306
|
+
}
|
|
66
307
|
var CurrentUser = common.createParamDecorator(
|
|
67
308
|
(_data, ctx) => {
|
|
68
309
|
const request = ctx.switchToHttp().getRequest();
|
|
@@ -71,8 +312,11 @@ var CurrentUser = common.createParamDecorator(
|
|
|
71
312
|
);
|
|
72
313
|
exports.JwtAuthGuard = class JwtAuthGuard extends passport.AuthGuard("jwt") {
|
|
73
314
|
handleRequest(err, user, _info) {
|
|
74
|
-
if (err != null
|
|
75
|
-
throw err
|
|
315
|
+
if (err != null) {
|
|
316
|
+
throw err;
|
|
317
|
+
}
|
|
318
|
+
if (!user) {
|
|
319
|
+
throw new common.UnauthorizedException(AuthErrorCode.UNAUTHORIZED);
|
|
76
320
|
}
|
|
77
321
|
return user;
|
|
78
322
|
}
|
|
@@ -91,7 +335,6 @@ var DUMMY_PASSWORD_HASH = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZd
|
|
|
91
335
|
exports.DefaultAuthHooks = class DefaultAuthHooks {
|
|
92
336
|
async buildJwtPayload(account) {
|
|
93
337
|
return {
|
|
94
|
-
sub: account.id,
|
|
95
338
|
...account.email != null ? { email: account.email } : {},
|
|
96
339
|
...account.username != null ? { username: account.username } : {}
|
|
97
340
|
};
|
|
@@ -158,49 +401,133 @@ function expiresAtFromTtlMs(ttlMs) {
|
|
|
158
401
|
}
|
|
159
402
|
var DEFAULT_EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
160
403
|
var DEFAULT_PASSWORD_RESET_TTL_MS = 15 * 60 * 1e3;
|
|
404
|
+
var JWT_TOKEN_TYPE = {
|
|
405
|
+
ACCESS: "access",
|
|
406
|
+
REFRESH: "refresh"
|
|
407
|
+
};
|
|
408
|
+
function issuerAudienceClaims(options) {
|
|
409
|
+
return {
|
|
410
|
+
...options.jwtIssuer != null ? { iss: options.jwtIssuer } : {},
|
|
411
|
+
...options.jwtAudience != null ? { aud: options.jwtAudience } : {}
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
function buildAccessClaims(hookClaims, sub, pwdAt, options) {
|
|
415
|
+
return {
|
|
416
|
+
...hookClaims,
|
|
417
|
+
sub,
|
|
418
|
+
typ: JWT_TOKEN_TYPE.ACCESS,
|
|
419
|
+
...pwdAt != null ? { pwdAt } : {},
|
|
420
|
+
...issuerAudienceClaims(options)
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function buildRefreshClaims(sub, pwdAt, options) {
|
|
424
|
+
return {
|
|
425
|
+
sub,
|
|
426
|
+
typ: JWT_TOKEN_TYPE.REFRESH,
|
|
427
|
+
jti: crypto.randomUUID(),
|
|
428
|
+
...pwdAt != null ? { pwdAt } : {},
|
|
429
|
+
...issuerAudienceClaims(options)
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
function assertIssuerAudience(payload, options, errorCode) {
|
|
433
|
+
if (options.jwtIssuer != null && payload.iss != null && payload.iss !== options.jwtIssuer) {
|
|
434
|
+
throw new common.UnauthorizedException(errorCode);
|
|
435
|
+
}
|
|
436
|
+
if (options.jwtAudience != null && payload.aud != null && payload.aud !== options.jwtAudience) {
|
|
437
|
+
throw new common.UnauthorizedException(errorCode);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
function assertAccessTokenClaims(payload, options) {
|
|
441
|
+
if (payload.typ != null && payload.typ !== JWT_TOKEN_TYPE.ACCESS) {
|
|
442
|
+
throw new common.UnauthorizedException(AuthErrorCode.INVALID_CREDENTIALS);
|
|
443
|
+
}
|
|
444
|
+
assertIssuerAudience(payload, options, AuthErrorCode.INVALID_CREDENTIALS);
|
|
445
|
+
}
|
|
446
|
+
function assertRefreshTokenClaims(payload, options) {
|
|
447
|
+
if (payload.typ !== JWT_TOKEN_TYPE.REFRESH) {
|
|
448
|
+
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
449
|
+
}
|
|
450
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
451
|
+
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
452
|
+
}
|
|
453
|
+
assertIssuerAudience(payload, options, AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
454
|
+
return payload;
|
|
455
|
+
}
|
|
456
|
+
var HMAC_ALGORITHM = "sha256";
|
|
457
|
+
function hashRefreshTokenValue(refreshToken, secret) {
|
|
458
|
+
return crypto.createHmac(HMAC_ALGORITHM, secret).update(refreshToken).digest("hex");
|
|
459
|
+
}
|
|
460
|
+
function compareRefreshTokenValue(refreshToken, storedHash, secret) {
|
|
461
|
+
const computed = hashRefreshTokenValue(refreshToken, secret);
|
|
462
|
+
try {
|
|
463
|
+
const a = Buffer.from(computed, "hex");
|
|
464
|
+
const b = Buffer.from(storedHash, "hex");
|
|
465
|
+
if (a.length !== b.length) return false;
|
|
466
|
+
return crypto.timingSafeEqual(a, b);
|
|
467
|
+
} catch {
|
|
468
|
+
return false;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
// src/services/token.service.ts
|
|
161
473
|
var JWT_ALGORITHM = "HS256";
|
|
162
474
|
exports.TokenService = class TokenService {
|
|
163
475
|
constructor(jwtService, options) {
|
|
164
476
|
this.jwtService = jwtService;
|
|
165
477
|
this.options = options;
|
|
166
478
|
}
|
|
167
|
-
|
|
168
|
-
return
|
|
479
|
+
signOptions(secret, expiresIn) {
|
|
480
|
+
return {
|
|
481
|
+
secret,
|
|
482
|
+
expiresIn,
|
|
483
|
+
algorithm: JWT_ALGORITHM,
|
|
484
|
+
...this.options.jwtIssuer != null ? { issuer: this.options.jwtIssuer } : {},
|
|
485
|
+
...this.options.jwtAudience != null ? { audience: this.options.jwtAudience } : {}
|
|
486
|
+
};
|
|
169
487
|
}
|
|
170
|
-
async signTokens(
|
|
488
|
+
async signTokens(accessClaims, refreshClaims) {
|
|
171
489
|
const accessExpiresIn = this.options.expiresIn ?? "1h";
|
|
172
490
|
const refreshExpiresIn = this.options.refreshExpiresIn ?? "7d";
|
|
173
491
|
const [accessToken, refreshToken] = await Promise.all([
|
|
174
492
|
this.jwtService.signAsync(
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
secret: this.options.secret,
|
|
178
|
-
expiresIn: accessExpiresIn,
|
|
179
|
-
algorithm: JWT_ALGORITHM
|
|
180
|
-
}
|
|
493
|
+
accessClaims,
|
|
494
|
+
this.signOptions(this.options.secret, accessExpiresIn)
|
|
181
495
|
),
|
|
182
496
|
this.jwtService.signAsync(
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
secret: this.options.refreshSecret,
|
|
186
|
-
expiresIn: refreshExpiresIn,
|
|
187
|
-
algorithm: JWT_ALGORITHM
|
|
188
|
-
}
|
|
497
|
+
refreshClaims,
|
|
498
|
+
this.signOptions(this.options.refreshSecret, refreshExpiresIn)
|
|
189
499
|
)
|
|
190
500
|
]);
|
|
191
501
|
return { accessToken, refreshToken };
|
|
192
502
|
}
|
|
193
503
|
async verifyRefreshToken(refreshToken) {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
504
|
+
try {
|
|
505
|
+
const payload = await this.jwtService.verifyAsync(
|
|
506
|
+
refreshToken,
|
|
507
|
+
{
|
|
508
|
+
secret: this.options.refreshSecret,
|
|
509
|
+
algorithms: [JWT_ALGORITHM],
|
|
510
|
+
...this.options.jwtIssuer != null ? { issuer: this.options.jwtIssuer } : {},
|
|
511
|
+
...this.options.jwtAudience != null ? { audience: this.options.jwtAudience } : {}
|
|
512
|
+
}
|
|
513
|
+
);
|
|
514
|
+
return assertRefreshTokenClaims(payload, this.options);
|
|
515
|
+
} catch (error) {
|
|
516
|
+
if (error instanceof common.UnauthorizedException) {
|
|
517
|
+
throw error;
|
|
518
|
+
}
|
|
519
|
+
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
520
|
+
}
|
|
198
521
|
}
|
|
199
522
|
async hashRefreshToken(refreshToken) {
|
|
200
|
-
return
|
|
523
|
+
return hashRefreshTokenValue(refreshToken, this.options.refreshSecret);
|
|
201
524
|
}
|
|
202
|
-
async compareRefreshToken(refreshToken,
|
|
203
|
-
return
|
|
525
|
+
async compareRefreshToken(refreshToken, hash2) {
|
|
526
|
+
return compareRefreshTokenValue(
|
|
527
|
+
refreshToken,
|
|
528
|
+
hash2,
|
|
529
|
+
this.options.refreshSecret
|
|
530
|
+
);
|
|
204
531
|
}
|
|
205
532
|
};
|
|
206
533
|
exports.TokenService = __decorateClass([
|
|
@@ -208,6 +535,32 @@ exports.TokenService = __decorateClass([
|
|
|
208
535
|
__decorateParam(0, common.Inject(jwt.JwtService)),
|
|
209
536
|
__decorateParam(1, common.Inject(AUTH_MODULE_OPTIONS))
|
|
210
537
|
], exports.TokenService);
|
|
538
|
+
function passwordChangedAtMs(account) {
|
|
539
|
+
return account.passwordChangedAt?.getTime();
|
|
540
|
+
}
|
|
541
|
+
function buildPwdAtClaim(account) {
|
|
542
|
+
return passwordChangedAtMs(account);
|
|
543
|
+
}
|
|
544
|
+
function isAccountLocked(account, lockoutEnabled) {
|
|
545
|
+
if (!lockoutEnabled) return false;
|
|
546
|
+
const lockedUntil = "lockedUntil" in account ? account.lockedUntil : void 0;
|
|
547
|
+
if (lockedUntil == null) return false;
|
|
548
|
+
return lockedUntil > /* @__PURE__ */ new Date();
|
|
549
|
+
}
|
|
550
|
+
function assertAccountNotLocked(account, options) {
|
|
551
|
+
if (!isAccountLocked(account, options.features?.accountLockout === true)) {
|
|
552
|
+
return;
|
|
553
|
+
}
|
|
554
|
+
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_LOCKED);
|
|
555
|
+
}
|
|
556
|
+
function assertPasswordNotStale(payload, account) {
|
|
557
|
+
const changedAt = passwordChangedAtMs(account);
|
|
558
|
+
if (changedAt == null) return;
|
|
559
|
+
const tokenPwdAt = typeof payload.pwdAt === "number" ? payload.pwdAt : void 0;
|
|
560
|
+
if (tokenPwdAt == null || tokenPwdAt < changedAt) {
|
|
561
|
+
throw new common.UnauthorizedException(AuthErrorCode.PASSWORD_CHANGED);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
211
564
|
|
|
212
565
|
// src/services/auth.service.ts
|
|
213
566
|
exports.AuthService = class AuthService {
|
|
@@ -256,7 +609,7 @@ exports.AuthService = class AuthService {
|
|
|
256
609
|
resolveRegisterIdentifier(input, this.identifierField);
|
|
257
610
|
this.assertRegisterEmailWhenVerificationEnabled(input);
|
|
258
611
|
this.assertPasswordPolicy(dto.password);
|
|
259
|
-
const passwordHash = await
|
|
612
|
+
const passwordHash = await bcrypt__namespace.hash(dto.password, this.bcryptRounds);
|
|
260
613
|
try {
|
|
261
614
|
const account = await this.authRepository.create({
|
|
262
615
|
...input,
|
|
@@ -280,11 +633,8 @@ exports.AuthService = class AuthService {
|
|
|
280
633
|
const account = await this.authRepository.findByIdentifierWithSecrets(
|
|
281
634
|
identifier
|
|
282
635
|
);
|
|
283
|
-
if (account != null) {
|
|
284
|
-
this.assertAccountNotLocked(account);
|
|
285
|
-
}
|
|
286
636
|
const passwordHash = account?.passwordHash ?? DUMMY_PASSWORD_HASH;
|
|
287
|
-
const passwordMatches = await
|
|
637
|
+
const passwordMatches = await bcrypt__namespace.compare(dto.password, passwordHash);
|
|
288
638
|
if (account?.passwordHash == null || !passwordMatches) {
|
|
289
639
|
if (account != null && this.accountLockoutEnabled) {
|
|
290
640
|
await this.authRepository.recordLoginFailure(
|
|
@@ -294,6 +644,7 @@ exports.AuthService = class AuthService {
|
|
|
294
644
|
}
|
|
295
645
|
throw new common.UnauthorizedException(AuthErrorCode.INVALID_CREDENTIALS);
|
|
296
646
|
}
|
|
647
|
+
assertAccountNotLocked(account, this.options);
|
|
297
648
|
this.assertAccountActive(account);
|
|
298
649
|
await this.authRepository.recordLoginSuccess(account.id);
|
|
299
650
|
return this.issueTokens(account);
|
|
@@ -302,14 +653,25 @@ exports.AuthService = class AuthService {
|
|
|
302
653
|
let payload;
|
|
303
654
|
try {
|
|
304
655
|
payload = await this.tokenService.verifyRefreshToken(refreshToken);
|
|
305
|
-
} catch {
|
|
656
|
+
} catch (error) {
|
|
657
|
+
if (error instanceof common.UnauthorizedException) {
|
|
658
|
+
throw error;
|
|
659
|
+
}
|
|
306
660
|
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
307
661
|
}
|
|
308
662
|
const account = await this.authRepository.findByIdWithSecrets(payload.sub);
|
|
309
663
|
if (account == null) {
|
|
310
664
|
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
311
665
|
}
|
|
666
|
+
assertPasswordNotStale(
|
|
667
|
+
{
|
|
668
|
+
sub: payload.sub,
|
|
669
|
+
...payload.pwdAt != null ? { pwdAt: payload.pwdAt } : {}
|
|
670
|
+
},
|
|
671
|
+
account
|
|
672
|
+
);
|
|
312
673
|
this.assertAccountActive(account);
|
|
674
|
+
assertAccountNotLocked(account, this.options);
|
|
313
675
|
if (this.rotateRefreshToken) {
|
|
314
676
|
if (account.refreshTokenHash == null) {
|
|
315
677
|
throw new common.UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
@@ -322,6 +684,9 @@ exports.AuthService = class AuthService {
|
|
|
322
684
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
323
685
|
throw new common.UnauthorizedException(AuthErrorCode.REFRESH_TOKEN_REUSE);
|
|
324
686
|
}
|
|
687
|
+
return this.issueTokens(account, {
|
|
688
|
+
expectedRefreshHash: account.refreshTokenHash
|
|
689
|
+
});
|
|
325
690
|
}
|
|
326
691
|
return this.issueTokens(account);
|
|
327
692
|
}
|
|
@@ -332,7 +697,7 @@ exports.AuthService = class AuthService {
|
|
|
332
697
|
async me(authId) {
|
|
333
698
|
const account = await this.authRepository.findById(authId);
|
|
334
699
|
if (account == null) {
|
|
335
|
-
throw new common.UnauthorizedException(
|
|
700
|
+
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_NOT_FOUND);
|
|
336
701
|
}
|
|
337
702
|
if (this.hooks.enrichMe != null) {
|
|
338
703
|
return this.hooks.enrichMe(account);
|
|
@@ -373,18 +738,37 @@ exports.AuthService = class AuthService {
|
|
|
373
738
|
throw new common.BadRequestException(AuthErrorCode.TOKEN_INVALID_OR_EXPIRED);
|
|
374
739
|
}
|
|
375
740
|
this.assertPasswordPolicy(newPassword);
|
|
376
|
-
const
|
|
741
|
+
const samePassword = await bcrypt__namespace.compare(
|
|
742
|
+
newPassword,
|
|
743
|
+
account.passwordHash
|
|
744
|
+
);
|
|
745
|
+
if (samePassword) {
|
|
746
|
+
throw new common.BadRequestException(AuthErrorCode.PASSWORD_UNCHANGED);
|
|
747
|
+
}
|
|
748
|
+
const passwordHash = await bcrypt__namespace.hash(newPassword, this.bcryptRounds);
|
|
377
749
|
await this.authRepository.updatePasswordHash(account.id, passwordHash);
|
|
378
750
|
await this.authRepository.clearResetToken(account.id);
|
|
379
751
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
380
752
|
return { reset: true };
|
|
381
753
|
}
|
|
754
|
+
async resendVerification(email) {
|
|
755
|
+
this.assertEmailVerificationEnabled();
|
|
756
|
+
this.assertEmailHookWhenVerificationEnabled();
|
|
757
|
+
const normalizedEmail = normalizeIdentifier(email);
|
|
758
|
+
const account = await this.authRepository.findUnverifiedByEmail(
|
|
759
|
+
normalizedEmail
|
|
760
|
+
);
|
|
761
|
+
if (account != null && !account.disabled) {
|
|
762
|
+
await this.sendVerificationEmail(account);
|
|
763
|
+
}
|
|
764
|
+
return { sent: true };
|
|
765
|
+
}
|
|
382
766
|
async changePassword(authId, currentPassword, newPassword) {
|
|
383
767
|
const account = await this.authRepository.findByIdWithSecrets(authId);
|
|
384
768
|
if (account?.passwordHash == null) {
|
|
385
769
|
throw new common.UnauthorizedException(AuthErrorCode.INVALID_CURRENT_PASSWORD);
|
|
386
770
|
}
|
|
387
|
-
const currentMatches = await
|
|
771
|
+
const currentMatches = await bcrypt__namespace.compare(
|
|
388
772
|
currentPassword,
|
|
389
773
|
account.passwordHash
|
|
390
774
|
);
|
|
@@ -395,19 +779,11 @@ exports.AuthService = class AuthService {
|
|
|
395
779
|
throw new common.BadRequestException(AuthErrorCode.PASSWORD_UNCHANGED);
|
|
396
780
|
}
|
|
397
781
|
this.assertPasswordPolicy(newPassword);
|
|
398
|
-
const passwordHash = await
|
|
782
|
+
const passwordHash = await bcrypt__namespace.hash(newPassword, this.bcryptRounds);
|
|
399
783
|
await this.authRepository.updatePasswordHash(account.id, passwordHash);
|
|
400
784
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
401
785
|
return { changed: true };
|
|
402
786
|
}
|
|
403
|
-
assertAccountNotLocked(account) {
|
|
404
|
-
if (!this.accountLockoutEnabled || account.lockedUntil == null) {
|
|
405
|
-
return;
|
|
406
|
-
}
|
|
407
|
-
if (account.lockedUntil > /* @__PURE__ */ new Date()) {
|
|
408
|
-
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_LOCKED);
|
|
409
|
-
}
|
|
410
|
-
}
|
|
411
787
|
assertAccountActive(account) {
|
|
412
788
|
if (account.disabled) {
|
|
413
789
|
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_DISABLED);
|
|
@@ -421,20 +797,33 @@ exports.AuthService = class AuthService {
|
|
|
421
797
|
assertPasswordComplexity(password);
|
|
422
798
|
}
|
|
423
799
|
}
|
|
424
|
-
async issueTokens(account) {
|
|
425
|
-
const
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
800
|
+
async issueTokens(account, rotation) {
|
|
801
|
+
const hookPayload = await this.hooks.buildJwtPayload(account);
|
|
802
|
+
const pwdAt = buildPwdAtClaim(account);
|
|
803
|
+
const tokens = await this.tokenService.signTokens(
|
|
804
|
+
buildAccessClaims(hookPayload, account.id, pwdAt, this.options),
|
|
805
|
+
buildRefreshClaims(account.id, pwdAt, this.options)
|
|
806
|
+
);
|
|
430
807
|
if (this.rotateRefreshToken) {
|
|
431
808
|
const refreshTokenHash = await this.tokenService.hashRefreshToken(
|
|
432
809
|
tokens.refreshToken
|
|
433
810
|
);
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
811
|
+
if (rotation?.expectedRefreshHash != null) {
|
|
812
|
+
const swapped = await this.authRepository.rotateRefreshTokenHashIfMatch(
|
|
813
|
+
account.id,
|
|
814
|
+
rotation.expectedRefreshHash,
|
|
815
|
+
refreshTokenHash
|
|
816
|
+
);
|
|
817
|
+
if (!swapped) {
|
|
818
|
+
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
819
|
+
throw new common.UnauthorizedException(AuthErrorCode.REFRESH_TOKEN_REUSE);
|
|
820
|
+
}
|
|
821
|
+
} else {
|
|
822
|
+
await this.authRepository.updateRefreshTokenHash(
|
|
823
|
+
account.id,
|
|
824
|
+
refreshTokenHash
|
|
825
|
+
);
|
|
826
|
+
}
|
|
438
827
|
}
|
|
439
828
|
await this.hooks.onAfterLogin?.(account);
|
|
440
829
|
return tokens;
|
|
@@ -504,6 +893,21 @@ exports.AuthService = __decorateClass([
|
|
|
504
893
|
__decorateParam(2, common.Inject(AUTH_HOOKS)),
|
|
505
894
|
__decorateParam(3, common.Inject(exports.TokenService))
|
|
506
895
|
], exports.AuthService);
|
|
896
|
+
function ApiAuthUnauthorizedResponse(...codes) {
|
|
897
|
+
const messageEnum = codes.length > 0 ? codes : Object.values(AuthErrorCode);
|
|
898
|
+
return common.applyDecorators(
|
|
899
|
+
swagger.ApiUnauthorizedResponse({
|
|
900
|
+
description: "Unauthorized \u2014 `message` is an `AuthErrorCode` value",
|
|
901
|
+
schema: {
|
|
902
|
+
type: "object",
|
|
903
|
+
properties: {
|
|
904
|
+
statusCode: { type: "number", example: 401 },
|
|
905
|
+
message: { type: "string", enum: messageEnum }
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
})
|
|
909
|
+
);
|
|
910
|
+
}
|
|
507
911
|
|
|
508
912
|
// src/controllers/auth.controller.ts
|
|
509
913
|
var AuthController = class {
|
|
@@ -528,6 +932,9 @@ var AuthController = class {
|
|
|
528
932
|
verifyEmail(dto) {
|
|
529
933
|
return this.authService.verifyEmail(dto.token);
|
|
530
934
|
}
|
|
935
|
+
resendVerification(dto) {
|
|
936
|
+
return this.authService.resendVerification(dto.email);
|
|
937
|
+
}
|
|
531
938
|
forgotPassword(dto) {
|
|
532
939
|
return this.authService.forgotPassword(dto.email);
|
|
533
940
|
}
|
|
@@ -544,52 +951,118 @@ var AuthController = class {
|
|
|
544
951
|
};
|
|
545
952
|
__decorateClass([
|
|
546
953
|
common.Post("register"),
|
|
954
|
+
swagger.ApiOperation({ summary: "Register a new account" }),
|
|
955
|
+
swagger.ApiResponse({ status: 201, type: RegisterAckDto }),
|
|
547
956
|
__decorateParam(0, common.Body())
|
|
548
957
|
], AuthController.prototype, "register", 1);
|
|
549
958
|
__decorateClass([
|
|
550
959
|
common.Post("login"),
|
|
960
|
+
common.HttpCode(common.HttpStatus.OK),
|
|
961
|
+
swagger.ApiOperation({ summary: "Login and receive JWT tokens" }),
|
|
962
|
+
swagger.ApiResponse({ status: 200, type: AuthTokensDto }),
|
|
963
|
+
ApiAuthUnauthorizedResponse(
|
|
964
|
+
AuthErrorCode.INVALID_CREDENTIALS,
|
|
965
|
+
AuthErrorCode.ACCOUNT_LOCKED,
|
|
966
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
967
|
+
AuthErrorCode.ACCOUNT_DISABLED
|
|
968
|
+
),
|
|
551
969
|
__decorateParam(0, common.Body())
|
|
552
970
|
], AuthController.prototype, "login", 1);
|
|
553
971
|
__decorateClass([
|
|
554
972
|
common.Post("refresh"),
|
|
555
973
|
common.HttpCode(common.HttpStatus.OK),
|
|
974
|
+
swagger.ApiOperation({ summary: "Refresh access token using refresh token" }),
|
|
975
|
+
swagger.ApiResponse({ status: 200, type: AuthTokensDto }),
|
|
976
|
+
ApiAuthUnauthorizedResponse(
|
|
977
|
+
AuthErrorCode.INVALID_REFRESH_TOKEN,
|
|
978
|
+
AuthErrorCode.REFRESH_TOKEN_REUSE,
|
|
979
|
+
AuthErrorCode.PASSWORD_CHANGED,
|
|
980
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
981
|
+
AuthErrorCode.ACCOUNT_LOCKED
|
|
982
|
+
),
|
|
556
983
|
__decorateParam(0, common.Body())
|
|
557
984
|
], AuthController.prototype, "refresh", 1);
|
|
558
985
|
__decorateClass([
|
|
559
986
|
common.Post("logout"),
|
|
560
987
|
common.UseGuards(exports.JwtAuthGuard),
|
|
988
|
+
swagger.ApiBearerAuth("access-token"),
|
|
561
989
|
common.HttpCode(common.HttpStatus.OK),
|
|
990
|
+
swagger.ApiOperation({ summary: "Logout and revoke refresh token" }),
|
|
991
|
+
swagger.ApiResponse({ status: 200, schema: { example: { loggedOut: true } } }),
|
|
992
|
+
ApiAuthUnauthorizedResponse(AuthErrorCode.UNAUTHORIZED),
|
|
562
993
|
__decorateParam(0, CurrentUser())
|
|
563
994
|
], AuthController.prototype, "logout", 1);
|
|
564
995
|
__decorateClass([
|
|
565
996
|
common.Get("me"),
|
|
566
997
|
common.UseGuards(exports.JwtAuthGuard),
|
|
998
|
+
swagger.ApiBearerAuth("access-token"),
|
|
999
|
+
swagger.ApiOperation({ summary: "Get current authenticated user profile" }),
|
|
1000
|
+
swagger.ApiResponse({ status: 200, type: MeResponseDto }),
|
|
1001
|
+
ApiAuthUnauthorizedResponse(
|
|
1002
|
+
AuthErrorCode.UNAUTHORIZED,
|
|
1003
|
+
AuthErrorCode.ACCOUNT_NOT_FOUND,
|
|
1004
|
+
AuthErrorCode.PASSWORD_CHANGED,
|
|
1005
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
1006
|
+
AuthErrorCode.ACCOUNT_LOCKED
|
|
1007
|
+
),
|
|
567
1008
|
__decorateParam(0, CurrentUser())
|
|
568
1009
|
], AuthController.prototype, "me", 1);
|
|
569
1010
|
__decorateClass([
|
|
570
1011
|
common.Post("verify-email"),
|
|
571
1012
|
common.HttpCode(common.HttpStatus.OK),
|
|
1013
|
+
swagger.ApiOperation({
|
|
1014
|
+
summary: "Verify email with token (requires emailVerification feature)"
|
|
1015
|
+
}),
|
|
1016
|
+
swagger.ApiResponse({ status: 200, schema: { example: { verified: true } } }),
|
|
1017
|
+
swagger.ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
572
1018
|
__decorateParam(0, common.Body())
|
|
573
1019
|
], AuthController.prototype, "verifyEmail", 1);
|
|
1020
|
+
__decorateClass([
|
|
1021
|
+
common.Post("resend-verification"),
|
|
1022
|
+
common.HttpCode(common.HttpStatus.OK),
|
|
1023
|
+
swagger.ApiOperation({
|
|
1024
|
+
summary: "Resend verification email (requires emailVerification feature)"
|
|
1025
|
+
}),
|
|
1026
|
+
swagger.ApiResponse({ status: 200, schema: { example: { sent: true } } }),
|
|
1027
|
+
swagger.ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
1028
|
+
__decorateParam(0, common.Body())
|
|
1029
|
+
], AuthController.prototype, "resendVerification", 1);
|
|
574
1030
|
__decorateClass([
|
|
575
1031
|
common.Post("forgot-password"),
|
|
576
1032
|
common.HttpCode(common.HttpStatus.OK),
|
|
1033
|
+
swagger.ApiOperation({
|
|
1034
|
+
summary: "Request password reset email (requires passwordReset feature)"
|
|
1035
|
+
}),
|
|
1036
|
+
swagger.ApiResponse({ status: 200, schema: { example: { sent: true } } }),
|
|
1037
|
+
swagger.ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
577
1038
|
__decorateParam(0, common.Body())
|
|
578
1039
|
], AuthController.prototype, "forgotPassword", 1);
|
|
579
1040
|
__decorateClass([
|
|
580
1041
|
common.Post("reset-password"),
|
|
581
1042
|
common.HttpCode(common.HttpStatus.OK),
|
|
1043
|
+
swagger.ApiOperation({
|
|
1044
|
+
summary: "Reset password with token (requires passwordReset feature)"
|
|
1045
|
+
}),
|
|
1046
|
+
swagger.ApiResponse({ status: 200, schema: { example: { reset: true } } }),
|
|
1047
|
+
swagger.ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
582
1048
|
__decorateParam(0, common.Body())
|
|
583
1049
|
], AuthController.prototype, "resetPassword", 1);
|
|
584
1050
|
__decorateClass([
|
|
585
1051
|
common.Post("change-password"),
|
|
586
1052
|
common.UseGuards(exports.JwtAuthGuard),
|
|
1053
|
+
swagger.ApiBearerAuth("access-token"),
|
|
587
1054
|
common.HttpCode(common.HttpStatus.OK),
|
|
1055
|
+
swagger.ApiOperation({ summary: "Change password for authenticated user" }),
|
|
1056
|
+
swagger.ApiResponse({ status: 200, schema: { example: { changed: true } } }),
|
|
1057
|
+
ApiAuthUnauthorizedResponse(
|
|
1058
|
+
AuthErrorCode.UNAUTHORIZED,
|
|
1059
|
+
AuthErrorCode.INVALID_CURRENT_PASSWORD
|
|
1060
|
+
),
|
|
588
1061
|
__decorateParam(0, CurrentUser()),
|
|
589
1062
|
__decorateParam(1, common.Body())
|
|
590
1063
|
], AuthController.prototype, "changePassword", 1);
|
|
591
1064
|
AuthController = __decorateClass([
|
|
592
|
-
|
|
1065
|
+
swagger.ApiTags("auth"),
|
|
593
1066
|
__decorateParam(0, common.Inject(exports.AuthService))
|
|
594
1067
|
], AuthController);
|
|
595
1068
|
|
|
@@ -611,19 +1084,55 @@ var JwtStrategy = class extends passport.PassportStrategy(passportJwt.Strategy)
|
|
|
611
1084
|
jwtFromRequest: passportJwt.ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
612
1085
|
ignoreExpiration: false,
|
|
613
1086
|
secretOrKey: options.secret,
|
|
614
|
-
algorithms: ["HS256"]
|
|
1087
|
+
algorithms: ["HS256"],
|
|
1088
|
+
...options.jwtIssuer != null ? { issuer: options.jwtIssuer } : {},
|
|
1089
|
+
...options.jwtAudience != null ? { audience: options.jwtAudience } : {}
|
|
615
1090
|
});
|
|
616
1091
|
this.options = options;
|
|
617
1092
|
this.authRepository = authRepository;
|
|
1093
|
+
this.validationCache = /* @__PURE__ */ new Map();
|
|
618
1094
|
}
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
1095
|
+
get cacheTtlMs() {
|
|
1096
|
+
return this.options.jwtValidationCacheTtlMs ?? 0;
|
|
1097
|
+
}
|
|
1098
|
+
getCachedAccount(sub) {
|
|
1099
|
+
const cached = this.validationCache.get(sub);
|
|
1100
|
+
if (cached == null || cached.expiresAt <= Date.now()) {
|
|
1101
|
+
if (cached != null) this.validationCache.delete(sub);
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
return cached.account;
|
|
1105
|
+
}
|
|
1106
|
+
cacheAccount(sub, account) {
|
|
1107
|
+
if (this.cacheTtlMs <= 0) return;
|
|
1108
|
+
this.validationCache.set(sub, {
|
|
1109
|
+
account,
|
|
1110
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
1111
|
+
});
|
|
1112
|
+
}
|
|
1113
|
+
assertAccountActive(account, payload) {
|
|
1114
|
+
if (account.disabled) {
|
|
1115
|
+
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_DISABLED);
|
|
623
1116
|
}
|
|
1117
|
+
assertAccountNotLocked(account, this.options);
|
|
624
1118
|
if (this.options.features?.emailVerification === true && !account.emailVerified) {
|
|
625
1119
|
throw new common.UnauthorizedException(AuthErrorCode.EMAIL_NOT_VERIFIED);
|
|
626
1120
|
}
|
|
1121
|
+
assertPasswordNotStale(payload, account);
|
|
1122
|
+
}
|
|
1123
|
+
async validate(payload) {
|
|
1124
|
+
assertAccessTokenClaims(payload, this.options);
|
|
1125
|
+
const cached = this.getCachedAccount(payload.sub);
|
|
1126
|
+
if (cached != null) {
|
|
1127
|
+
this.assertAccountActive(cached, payload);
|
|
1128
|
+
return payload;
|
|
1129
|
+
}
|
|
1130
|
+
const account = await this.authRepository.findById(payload.sub);
|
|
1131
|
+
if (account == null) {
|
|
1132
|
+
throw new common.UnauthorizedException(AuthErrorCode.ACCOUNT_NOT_FOUND);
|
|
1133
|
+
}
|
|
1134
|
+
this.assertAccountActive(account, payload);
|
|
1135
|
+
this.cacheAccount(payload.sub, account);
|
|
627
1136
|
return payload;
|
|
628
1137
|
}
|
|
629
1138
|
};
|
|
@@ -632,8 +1141,6 @@ JwtStrategy = __decorateClass([
|
|
|
632
1141
|
__decorateParam(0, common.Inject(AUTH_MODULE_OPTIONS)),
|
|
633
1142
|
__decorateParam(1, common.Inject(AUTH_REPOSITORY))
|
|
634
1143
|
], JwtStrategy);
|
|
635
|
-
|
|
636
|
-
// src/utils/hooks-provider.util.ts
|
|
637
1144
|
function createHooksProvider(options) {
|
|
638
1145
|
if (options.hooksProvider != null) {
|
|
639
1146
|
return options.hooksProvider;
|
|
@@ -641,7 +1148,8 @@ function createHooksProvider(options) {
|
|
|
641
1148
|
const HooksClass = options.hooks ?? exports.DefaultAuthHooks;
|
|
642
1149
|
return {
|
|
643
1150
|
provide: AUTH_HOOKS,
|
|
644
|
-
|
|
1151
|
+
inject: [core.ModuleRef],
|
|
1152
|
+
useFactory: (moduleRef) => moduleRef.create(HooksClass)
|
|
645
1153
|
};
|
|
646
1154
|
}
|
|
647
1155
|
|
|
@@ -681,6 +1189,17 @@ function validateAuthModuleOptions(options) {
|
|
|
681
1189
|
"AuthModule: passwordResetTokenTtlMs must be at least 60000 (1 minute)"
|
|
682
1190
|
);
|
|
683
1191
|
}
|
|
1192
|
+
if (options.features?.refreshTokenRotation === false) {
|
|
1193
|
+
console.warn(
|
|
1194
|
+
"[aranza-auth] features.refreshTokenRotation is false \u2014 refresh tokens are stateless until JWT expiry; stolen tokens cannot be revoked server-side."
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
1197
|
+
const cacheTtl = options.jwtValidationCacheTtlMs ?? 0;
|
|
1198
|
+
if (cacheTtl < 0 || cacheTtl > 3e5) {
|
|
1199
|
+
throw new Error(
|
|
1200
|
+
"AuthModule: jwtValidationCacheTtlMs must be between 0 and 300000 (5 minutes)"
|
|
1201
|
+
);
|
|
1202
|
+
}
|
|
684
1203
|
}
|
|
685
1204
|
|
|
686
1205
|
// src/auth.module.ts
|
|
@@ -698,15 +1217,33 @@ function createCoreProviders(options) {
|
|
|
698
1217
|
exports.JwtAuthGuard
|
|
699
1218
|
];
|
|
700
1219
|
}
|
|
701
|
-
function
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1220
|
+
function createAsyncProviders(options) {
|
|
1221
|
+
return [
|
|
1222
|
+
{
|
|
1223
|
+
provide: AUTH_MODULE_OPTIONS,
|
|
1224
|
+
inject: options.inject ?? [],
|
|
1225
|
+
useFactory: async (...args) => {
|
|
1226
|
+
const config = await options.useFactory(...args);
|
|
1227
|
+
validateAuthModuleOptions(config);
|
|
1228
|
+
return config;
|
|
1229
|
+
}
|
|
1230
|
+
},
|
|
1231
|
+
createHooksProvider(options),
|
|
1232
|
+
exports.AuthService,
|
|
1233
|
+
exports.TokenService,
|
|
1234
|
+
JwtStrategy,
|
|
1235
|
+
exports.JwtAuthGuard
|
|
1236
|
+
];
|
|
1237
|
+
}
|
|
1238
|
+
function jwtModuleOptions(opts) {
|
|
706
1239
|
return {
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
1240
|
+
secret: opts.secret,
|
|
1241
|
+
signOptions: {
|
|
1242
|
+
expiresIn: opts.expiresIn ?? "1h",
|
|
1243
|
+
algorithm: "HS256",
|
|
1244
|
+
...opts.jwtIssuer != null ? { issuer: opts.jwtIssuer } : {},
|
|
1245
|
+
...opts.jwtAudience != null ? { audience: opts.jwtAudience } : {}
|
|
1246
|
+
}
|
|
710
1247
|
};
|
|
711
1248
|
}
|
|
712
1249
|
function createAuthImports() {
|
|
@@ -714,13 +1251,7 @@ function createAuthImports() {
|
|
|
714
1251
|
passport.PassportModule.register({ defaultStrategy: "jwt" }),
|
|
715
1252
|
jwt.JwtModule.registerAsync({
|
|
716
1253
|
inject: [AUTH_MODULE_OPTIONS],
|
|
717
|
-
useFactory: (opts) => (
|
|
718
|
-
secret: opts.secret,
|
|
719
|
-
signOptions: {
|
|
720
|
-
expiresIn: opts.expiresIn ?? "1h",
|
|
721
|
-
algorithm: "HS256"
|
|
722
|
-
}
|
|
723
|
-
})
|
|
1254
|
+
useFactory: (opts) => jwtModuleOptions(opts)
|
|
724
1255
|
})
|
|
725
1256
|
];
|
|
726
1257
|
}
|
|
@@ -731,24 +1262,6 @@ function mergeImports(userImports) {
|
|
|
731
1262
|
}
|
|
732
1263
|
return merged;
|
|
733
1264
|
}
|
|
734
|
-
function createAsyncProviders(options) {
|
|
735
|
-
return [
|
|
736
|
-
{
|
|
737
|
-
provide: AUTH_MODULE_OPTIONS,
|
|
738
|
-
inject: options.inject ?? [],
|
|
739
|
-
useFactory: async (...args) => {
|
|
740
|
-
const config = await options.useFactory(...args);
|
|
741
|
-
validateAuthModuleOptions(config);
|
|
742
|
-
return config;
|
|
743
|
-
}
|
|
744
|
-
},
|
|
745
|
-
createAsyncHooksProvider(options),
|
|
746
|
-
exports.AuthService,
|
|
747
|
-
exports.TokenService,
|
|
748
|
-
JwtStrategy,
|
|
749
|
-
exports.JwtAuthGuard
|
|
750
|
-
];
|
|
751
|
-
}
|
|
752
1265
|
exports.AuthModule = class AuthModule {
|
|
753
1266
|
static forRoot(options) {
|
|
754
1267
|
const routePrefix = options.routePrefix ?? "auth";
|
|
@@ -793,93 +1306,10 @@ exports.AuthModule = __decorateClass([
|
|
|
793
1306
|
common.Module({})
|
|
794
1307
|
], exports.AuthModule);
|
|
795
1308
|
|
|
796
|
-
// src/dto/auth-tokens.dto.ts
|
|
797
|
-
var AuthTokensDto = class {
|
|
798
|
-
};
|
|
799
|
-
var ChangePasswordDto = class {
|
|
800
|
-
};
|
|
801
|
-
__decorateClass([
|
|
802
|
-
classValidator.IsString(),
|
|
803
|
-
classValidator.IsNotEmpty()
|
|
804
|
-
], ChangePasswordDto.prototype, "currentPassword", 2);
|
|
805
|
-
__decorateClass([
|
|
806
|
-
classValidator.IsString(),
|
|
807
|
-
classValidator.IsNotEmpty(),
|
|
808
|
-
classValidator.Length(8, 128)
|
|
809
|
-
], ChangePasswordDto.prototype, "newPassword", 2);
|
|
810
|
-
var ForgotPasswordDto = class {
|
|
811
|
-
};
|
|
812
|
-
__decorateClass([
|
|
813
|
-
classValidator.IsEmail()
|
|
814
|
-
], ForgotPasswordDto.prototype, "email", 2);
|
|
815
|
-
var LoginDto = class {
|
|
816
|
-
};
|
|
817
|
-
__decorateClass([
|
|
818
|
-
classValidator.IsOptional(),
|
|
819
|
-
classValidator.ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
820
|
-
classValidator.IsEmail(),
|
|
821
|
-
classValidator.Length(3, 255)
|
|
822
|
-
], LoginDto.prototype, "email", 2);
|
|
823
|
-
__decorateClass([
|
|
824
|
-
classValidator.IsOptional(),
|
|
825
|
-
classValidator.IsString(),
|
|
826
|
-
classValidator.Length(3, 50)
|
|
827
|
-
], LoginDto.prototype, "username", 2);
|
|
828
|
-
__decorateClass([
|
|
829
|
-
classValidator.IsString(),
|
|
830
|
-
classValidator.IsNotEmpty(),
|
|
831
|
-
classValidator.Length(8, 128)
|
|
832
|
-
], LoginDto.prototype, "password", 2);
|
|
833
|
-
var RefreshTokenDto = class {
|
|
834
|
-
};
|
|
835
|
-
__decorateClass([
|
|
836
|
-
classValidator.IsString(),
|
|
837
|
-
classValidator.IsNotEmpty()
|
|
838
|
-
], RefreshTokenDto.prototype, "refreshToken", 2);
|
|
839
|
-
|
|
840
|
-
// src/dto/register-ack.dto.ts
|
|
841
|
-
var RegisterAckDto = class {
|
|
842
|
-
};
|
|
843
|
-
var RegisterDto = class {
|
|
844
|
-
};
|
|
845
|
-
__decorateClass([
|
|
846
|
-
classValidator.IsOptional(),
|
|
847
|
-
classValidator.ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
848
|
-
classValidator.IsEmail(),
|
|
849
|
-
classValidator.Length(3, 255)
|
|
850
|
-
], RegisterDto.prototype, "email", 2);
|
|
851
|
-
__decorateClass([
|
|
852
|
-
classValidator.IsOptional(),
|
|
853
|
-
classValidator.IsString(),
|
|
854
|
-
classValidator.Length(3, 50),
|
|
855
|
-
classValidator.Matches(/^[a-zA-Z0-9._-]+$/)
|
|
856
|
-
], RegisterDto.prototype, "username", 2);
|
|
857
|
-
__decorateClass([
|
|
858
|
-
classValidator.IsString(),
|
|
859
|
-
classValidator.IsNotEmpty(),
|
|
860
|
-
classValidator.Length(8, 128)
|
|
861
|
-
], RegisterDto.prototype, "password", 2);
|
|
862
|
-
var ResetPasswordDto = class {
|
|
863
|
-
};
|
|
864
|
-
__decorateClass([
|
|
865
|
-
classValidator.IsString(),
|
|
866
|
-
classValidator.IsNotEmpty()
|
|
867
|
-
], ResetPasswordDto.prototype, "token", 2);
|
|
868
|
-
__decorateClass([
|
|
869
|
-
classValidator.IsString(),
|
|
870
|
-
classValidator.IsNotEmpty(),
|
|
871
|
-
classValidator.Length(8, 128)
|
|
872
|
-
], ResetPasswordDto.prototype, "newPassword", 2);
|
|
873
|
-
var VerifyEmailDto = class {
|
|
874
|
-
};
|
|
875
|
-
__decorateClass([
|
|
876
|
-
classValidator.IsString(),
|
|
877
|
-
classValidator.IsNotEmpty()
|
|
878
|
-
], VerifyEmailDto.prototype, "token", 2);
|
|
879
|
-
|
|
880
1309
|
exports.AUTH_HOOKS = AUTH_HOOKS;
|
|
881
1310
|
exports.AUTH_MODULE_OPTIONS = AUTH_MODULE_OPTIONS;
|
|
882
1311
|
exports.AUTH_RATE_LIMIT_PRESETS = AUTH_RATE_LIMIT_PRESETS;
|
|
1312
|
+
exports.AUTH_RATE_LIMIT_ROUTES = AUTH_RATE_LIMIT_ROUTES;
|
|
883
1313
|
exports.AUTH_REPOSITORY = AUTH_REPOSITORY;
|
|
884
1314
|
exports.AuthErrorCode = AuthErrorCode;
|
|
885
1315
|
exports.AuthTokensDto = AuthTokensDto;
|
|
@@ -887,10 +1317,15 @@ exports.ChangePasswordDto = ChangePasswordDto;
|
|
|
887
1317
|
exports.CurrentUser = CurrentUser;
|
|
888
1318
|
exports.ForgotPasswordDto = ForgotPasswordDto;
|
|
889
1319
|
exports.LoginDto = LoginDto;
|
|
1320
|
+
exports.MeResponseDto = MeResponseDto;
|
|
890
1321
|
exports.RefreshTokenDto = RefreshTokenDto;
|
|
891
1322
|
exports.RegisterAckDto = RegisterAckDto;
|
|
892
1323
|
exports.RegisterDto = RegisterDto;
|
|
1324
|
+
exports.ResendVerificationDto = ResendVerificationDto;
|
|
893
1325
|
exports.ResetPasswordDto = ResetPasswordDto;
|
|
894
1326
|
exports.VerifyEmailDto = VerifyEmailDto;
|
|
1327
|
+
exports.buildClearRefreshTokenCookie = buildClearRefreshTokenCookie;
|
|
1328
|
+
exports.buildRefreshTokenCookie = buildRefreshTokenCookie;
|
|
1329
|
+
exports.setupAuthSwagger = setupAuthSwagger;
|
|
895
1330
|
//# sourceMappingURL=index.cjs.map
|
|
896
1331
|
//# sourceMappingURL=index.cjs.map
|