@aranzatech/aranza-auth 0.2.0 → 0.2.2
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 +60 -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.js
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { __decorateClass, __decorateParam, AUTH_MODULE_OPTIONS, AUTH_REPOSITORY, AUTH_HOOKS, normalizeIdentifier, resolveRegisterIdentifier, readAccountIdentifier } from './chunk-QNEFN5ES.js';
|
|
2
2
|
export { AUTH_HOOKS, AUTH_MODULE_OPTIONS, AUTH_REPOSITORY } from './chunk-QNEFN5ES.js';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { ApiProperty, ApiPropertyOptional, ApiOperation, ApiResponse, ApiBearerAuth, ApiTags, ApiUnauthorizedResponse, DocumentBuilder, SwaggerModule } from '@nestjs/swagger';
|
|
4
|
+
import { IsString, IsNotEmpty, Length, IsEmail, IsOptional, ValidateIf, Matches } from 'class-validator';
|
|
5
|
+
import { createParamDecorator, UnauthorizedException, Injectable, Inject, Post, Body, HttpCode, HttpStatus, UseGuards, Get, Module, BadRequestException, NotFoundException, applyDecorators, Controller } from '@nestjs/common';
|
|
5
6
|
import { JwtService, JwtModule } from '@nestjs/jwt';
|
|
6
7
|
import { AuthGuard, PassportStrategy, PassportModule } from '@nestjs/passport';
|
|
7
|
-
import * as
|
|
8
|
-
import { createHash, randomBytes } from 'crypto';
|
|
8
|
+
import * as bcrypt from 'bcryptjs';
|
|
9
|
+
import { createHmac, timingSafeEqual, createHash, randomBytes, randomUUID } from 'crypto';
|
|
9
10
|
import { Strategy, ExtractJwt } from 'passport-jwt';
|
|
10
|
-
import {
|
|
11
|
+
import { ModuleRef } from '@nestjs/core';
|
|
11
12
|
|
|
12
13
|
// src/constants/rate-limit.presets.ts
|
|
13
14
|
var AUTH_RATE_LIMIT_PRESETS = {
|
|
@@ -19,18 +20,258 @@ var AUTH_RATE_LIMIT_PRESETS = {
|
|
|
19
20
|
passwordReset: { name: "auth-password-reset", ttl: 6e4, limit: 3 }
|
|
20
21
|
};
|
|
21
22
|
|
|
23
|
+
// src/constants/rate-limit.routes.ts
|
|
24
|
+
var AUTH_RATE_LIMIT_ROUTES = {
|
|
25
|
+
login: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
26
|
+
register: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
27
|
+
refresh: AUTH_RATE_LIMIT_PRESETS.credentials,
|
|
28
|
+
"forgot-password": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
29
|
+
"reset-password": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
30
|
+
"resend-verification": AUTH_RATE_LIMIT_PRESETS.passwordReset,
|
|
31
|
+
default: AUTH_RATE_LIMIT_PRESETS.default
|
|
32
|
+
};
|
|
33
|
+
|
|
22
34
|
// src/constants/auth-errors.ts
|
|
23
35
|
var AuthErrorCode = {
|
|
24
|
-
INVALID_CREDENTIALS: "
|
|
25
|
-
INVALID_REFRESH_TOKEN: "
|
|
36
|
+
INVALID_CREDENTIALS: "INVALID_CREDENTIALS",
|
|
37
|
+
INVALID_REFRESH_TOKEN: "INVALID_REFRESH_TOKEN",
|
|
26
38
|
REFRESH_TOKEN_REUSE: "REFRESH_TOKEN_REUSE",
|
|
27
39
|
ACCOUNT_DISABLED: "ACCOUNT_DISABLED",
|
|
40
|
+
ACCOUNT_NOT_FOUND: "ACCOUNT_NOT_FOUND",
|
|
28
41
|
EMAIL_NOT_VERIFIED: "EMAIL_NOT_VERIFIED",
|
|
29
42
|
TOKEN_INVALID_OR_EXPIRED: "TOKEN_INVALID_OR_EXPIRED",
|
|
30
43
|
ACCOUNT_LOCKED: "ACCOUNT_LOCKED",
|
|
31
44
|
INVALID_CURRENT_PASSWORD: "INVALID_CURRENT_PASSWORD",
|
|
32
|
-
PASSWORD_UNCHANGED: "PASSWORD_UNCHANGED"
|
|
45
|
+
PASSWORD_UNCHANGED: "PASSWORD_UNCHANGED",
|
|
46
|
+
PASSWORD_CHANGED: "PASSWORD_CHANGED",
|
|
47
|
+
/** Missing or invalid Bearer token on a protected route. */
|
|
48
|
+
UNAUTHORIZED: "UNAUTHORIZED"
|
|
49
|
+
};
|
|
50
|
+
var AuthTokensDto = class {
|
|
51
|
+
};
|
|
52
|
+
__decorateClass([
|
|
53
|
+
ApiProperty({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
|
54
|
+
], AuthTokensDto.prototype, "accessToken", 2);
|
|
55
|
+
__decorateClass([
|
|
56
|
+
ApiProperty({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." })
|
|
57
|
+
], AuthTokensDto.prototype, "refreshToken", 2);
|
|
58
|
+
var ChangePasswordDto = class {
|
|
59
|
+
};
|
|
60
|
+
__decorateClass([
|
|
61
|
+
ApiProperty({ example: "CurrentPassword1", minLength: 1, maxLength: 128 }),
|
|
62
|
+
IsString(),
|
|
63
|
+
IsNotEmpty(),
|
|
64
|
+
Length(1, 128)
|
|
65
|
+
], ChangePasswordDto.prototype, "currentPassword", 2);
|
|
66
|
+
__decorateClass([
|
|
67
|
+
ApiProperty({ example: "NewPassword1", minLength: 8, maxLength: 128 }),
|
|
68
|
+
IsString(),
|
|
69
|
+
IsNotEmpty(),
|
|
70
|
+
Length(8, 128)
|
|
71
|
+
], ChangePasswordDto.prototype, "newPassword", 2);
|
|
72
|
+
var ForgotPasswordDto = class {
|
|
73
|
+
};
|
|
74
|
+
__decorateClass([
|
|
75
|
+
ApiProperty({ example: "user@example.com" }),
|
|
76
|
+
IsEmail()
|
|
77
|
+
], ForgotPasswordDto.prototype, "email", 2);
|
|
78
|
+
var LoginDto = class {
|
|
79
|
+
};
|
|
80
|
+
__decorateClass([
|
|
81
|
+
ApiPropertyOptional({ example: "user@example.com" }),
|
|
82
|
+
IsOptional(),
|
|
83
|
+
ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
84
|
+
IsEmail(),
|
|
85
|
+
Length(3, 255)
|
|
86
|
+
], LoginDto.prototype, "email", 2);
|
|
87
|
+
__decorateClass([
|
|
88
|
+
ApiPropertyOptional({ example: "johndoe" }),
|
|
89
|
+
IsOptional(),
|
|
90
|
+
IsString(),
|
|
91
|
+
Length(3, 50)
|
|
92
|
+
], LoginDto.prototype, "username", 2);
|
|
93
|
+
__decorateClass([
|
|
94
|
+
ApiProperty({ example: "Password1", minLength: 8, maxLength: 128 }),
|
|
95
|
+
IsString(),
|
|
96
|
+
IsNotEmpty(),
|
|
97
|
+
Length(8, 128)
|
|
98
|
+
], LoginDto.prototype, "password", 2);
|
|
99
|
+
var MeResponseDto = class {
|
|
100
|
+
};
|
|
101
|
+
__decorateClass([
|
|
102
|
+
ApiProperty({ example: "507f1f77bcf86cd799439011" })
|
|
103
|
+
], MeResponseDto.prototype, "id", 2);
|
|
104
|
+
__decorateClass([
|
|
105
|
+
ApiPropertyOptional({ example: "user@example.com" })
|
|
106
|
+
], MeResponseDto.prototype, "email", 2);
|
|
107
|
+
__decorateClass([
|
|
108
|
+
ApiPropertyOptional({ example: "johndoe" })
|
|
109
|
+
], MeResponseDto.prototype, "username", 2);
|
|
110
|
+
__decorateClass([
|
|
111
|
+
ApiProperty({ example: true })
|
|
112
|
+
], MeResponseDto.prototype, "emailVerified", 2);
|
|
113
|
+
__decorateClass([
|
|
114
|
+
ApiProperty({ example: false })
|
|
115
|
+
], MeResponseDto.prototype, "disabled", 2);
|
|
116
|
+
__decorateClass([
|
|
117
|
+
ApiPropertyOptional({ type: String, format: "date-time" })
|
|
118
|
+
], MeResponseDto.prototype, "lastLoginAt", 2);
|
|
119
|
+
__decorateClass([
|
|
120
|
+
ApiPropertyOptional({ type: String, format: "date-time" })
|
|
121
|
+
], MeResponseDto.prototype, "passwordChangedAt", 2);
|
|
122
|
+
var RefreshTokenDto = class {
|
|
123
|
+
};
|
|
124
|
+
__decorateClass([
|
|
125
|
+
ApiProperty({ example: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." }),
|
|
126
|
+
IsString(),
|
|
127
|
+
IsNotEmpty()
|
|
128
|
+
], RefreshTokenDto.prototype, "refreshToken", 2);
|
|
129
|
+
var RegisterAckDto = class {
|
|
130
|
+
};
|
|
131
|
+
__decorateClass([
|
|
132
|
+
ApiProperty({ example: true, enum: [true] })
|
|
133
|
+
], RegisterAckDto.prototype, "registered", 2);
|
|
134
|
+
var RegisterDto = class {
|
|
135
|
+
};
|
|
136
|
+
__decorateClass([
|
|
137
|
+
ApiPropertyOptional({ example: "user@example.com" }),
|
|
138
|
+
IsOptional(),
|
|
139
|
+
ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
140
|
+
IsEmail(),
|
|
141
|
+
Length(3, 255)
|
|
142
|
+
], RegisterDto.prototype, "email", 2);
|
|
143
|
+
__decorateClass([
|
|
144
|
+
ApiPropertyOptional({ example: "johndoe" }),
|
|
145
|
+
IsOptional(),
|
|
146
|
+
IsString(),
|
|
147
|
+
Length(3, 50),
|
|
148
|
+
Matches(/^[a-zA-Z0-9._-]+$/)
|
|
149
|
+
], RegisterDto.prototype, "username", 2);
|
|
150
|
+
__decorateClass([
|
|
151
|
+
ApiProperty({ example: "Password1", minLength: 8, maxLength: 128 }),
|
|
152
|
+
IsString(),
|
|
153
|
+
IsNotEmpty(),
|
|
154
|
+
Length(8, 128)
|
|
155
|
+
], RegisterDto.prototype, "password", 2);
|
|
156
|
+
var ResendVerificationDto = class {
|
|
157
|
+
};
|
|
158
|
+
__decorateClass([
|
|
159
|
+
ApiProperty({ example: "user@example.com" }),
|
|
160
|
+
IsEmail()
|
|
161
|
+
], ResendVerificationDto.prototype, "email", 2);
|
|
162
|
+
var ResetPasswordDto = class {
|
|
163
|
+
};
|
|
164
|
+
__decorateClass([
|
|
165
|
+
ApiProperty({ example: "reset-token-from-email" }),
|
|
166
|
+
IsString(),
|
|
167
|
+
IsNotEmpty()
|
|
168
|
+
], ResetPasswordDto.prototype, "token", 2);
|
|
169
|
+
__decorateClass([
|
|
170
|
+
ApiProperty({ example: "NewPassword1", minLength: 8, maxLength: 128 }),
|
|
171
|
+
IsString(),
|
|
172
|
+
IsNotEmpty(),
|
|
173
|
+
Length(8, 128)
|
|
174
|
+
], ResetPasswordDto.prototype, "newPassword", 2);
|
|
175
|
+
var VerifyEmailDto = class {
|
|
33
176
|
};
|
|
177
|
+
__decorateClass([
|
|
178
|
+
ApiProperty({ example: "verification-token-from-email" }),
|
|
179
|
+
IsString(),
|
|
180
|
+
IsNotEmpty()
|
|
181
|
+
], VerifyEmailDto.prototype, "token", 2);
|
|
182
|
+
|
|
183
|
+
// src/swagger/setup-swagger.util.ts
|
|
184
|
+
var AUTH_SWAGGER_MODELS = [
|
|
185
|
+
AuthTokensDto,
|
|
186
|
+
ChangePasswordDto,
|
|
187
|
+
ForgotPasswordDto,
|
|
188
|
+
LoginDto,
|
|
189
|
+
MeResponseDto,
|
|
190
|
+
RefreshTokenDto,
|
|
191
|
+
RegisterAckDto,
|
|
192
|
+
RegisterDto,
|
|
193
|
+
ResendVerificationDto,
|
|
194
|
+
ResetPasswordDto,
|
|
195
|
+
VerifyEmailDto
|
|
196
|
+
];
|
|
197
|
+
function describeEnabledFeatures(features) {
|
|
198
|
+
const lines = [];
|
|
199
|
+
if (features.emailVerification === true) {
|
|
200
|
+
lines.push("- Email verification (`POST /auth/verify-email`, `POST /auth/resend-verification`)");
|
|
201
|
+
}
|
|
202
|
+
if (features.passwordReset === true) {
|
|
203
|
+
lines.push("- Password reset (`POST /auth/forgot-password`, `POST /auth/reset-password`)");
|
|
204
|
+
}
|
|
205
|
+
if (features.refreshTokenRotation === false) {
|
|
206
|
+
lines.push("- Refresh token rotation **disabled** (stateless refresh until JWT expiry)");
|
|
207
|
+
}
|
|
208
|
+
if (features.accountLockout === true) {
|
|
209
|
+
lines.push("- Account lockout after failed logins");
|
|
210
|
+
}
|
|
211
|
+
if (lines.length === 0) {
|
|
212
|
+
return "";
|
|
213
|
+
}
|
|
214
|
+
return `
|
|
215
|
+
|
|
216
|
+
## Auth features enabled
|
|
217
|
+
${lines.join("\n")}`;
|
|
218
|
+
}
|
|
219
|
+
function setupAuthSwagger(app, options = {}) {
|
|
220
|
+
const nestApp = app;
|
|
221
|
+
const baseDescription = options.description ?? "REST API with JWT authentication via @aranzatech/aranza-auth";
|
|
222
|
+
const config = new DocumentBuilder().setTitle(options.title ?? "API").setDescription(
|
|
223
|
+
`${baseDescription}${describeEnabledFeatures(options.features ?? {})}`
|
|
224
|
+
).setVersion(options.version ?? "1.0").addBearerAuth(
|
|
225
|
+
{
|
|
226
|
+
type: "http",
|
|
227
|
+
scheme: "bearer",
|
|
228
|
+
bearerFormat: "JWT",
|
|
229
|
+
description: "Access token from POST /auth/login"
|
|
230
|
+
},
|
|
231
|
+
"access-token"
|
|
232
|
+
).build();
|
|
233
|
+
const document = SwaggerModule.createDocument(nestApp, config, {
|
|
234
|
+
extraModels: [...AUTH_SWAGGER_MODELS]
|
|
235
|
+
});
|
|
236
|
+
if (options.exportPath != null) {
|
|
237
|
+
void import('fs/promises').then(
|
|
238
|
+
({ writeFile }) => writeFile(options.exportPath, JSON.stringify(document, null, 2), "utf8")
|
|
239
|
+
);
|
|
240
|
+
}
|
|
241
|
+
SwaggerModule.setup(options.path ?? "api", nestApp, document);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// src/utils/refresh-token-cookie.util.ts
|
|
245
|
+
var DEFAULT_COOKIE_NAME = "refresh_token";
|
|
246
|
+
var DEFAULT_MAX_AGE_SECONDS = 7 * 24 * 60 * 60;
|
|
247
|
+
function resolveCookieOptions(options = {}) {
|
|
248
|
+
return {
|
|
249
|
+
name: options.name ?? DEFAULT_COOKIE_NAME,
|
|
250
|
+
path: options.path ?? "/auth/refresh",
|
|
251
|
+
secure: options.secure ?? true,
|
|
252
|
+
sameSite: options.sameSite ?? "strict",
|
|
253
|
+
maxAgeSeconds: options.maxAgeSeconds ?? DEFAULT_MAX_AGE_SECONDS,
|
|
254
|
+
httpOnly: options.httpOnly ?? true
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
function formatCookieAttributes(options) {
|
|
258
|
+
const parts = [
|
|
259
|
+
`Path=${options.path}`,
|
|
260
|
+
`Max-Age=${options.maxAgeSeconds}`,
|
|
261
|
+
`SameSite=${options.sameSite}`
|
|
262
|
+
];
|
|
263
|
+
if (options.secure) parts.push("Secure");
|
|
264
|
+
if (options.httpOnly) parts.push("HttpOnly");
|
|
265
|
+
return parts.join("; ");
|
|
266
|
+
}
|
|
267
|
+
function buildRefreshTokenCookie(refreshToken, options = {}) {
|
|
268
|
+
const resolved = resolveCookieOptions(options);
|
|
269
|
+
return `${resolved.name}=${encodeURIComponent(refreshToken)}; ${formatCookieAttributes(resolved)}`;
|
|
270
|
+
}
|
|
271
|
+
function buildClearRefreshTokenCookie(options = {}) {
|
|
272
|
+
const resolved = resolveCookieOptions(options);
|
|
273
|
+
return `${resolved.name}=; Path=${resolved.path}; Max-Age=0; HttpOnly`;
|
|
274
|
+
}
|
|
34
275
|
var CurrentUser = createParamDecorator(
|
|
35
276
|
(_data, ctx) => {
|
|
36
277
|
const request = ctx.switchToHttp().getRequest();
|
|
@@ -39,8 +280,11 @@ var CurrentUser = createParamDecorator(
|
|
|
39
280
|
);
|
|
40
281
|
var JwtAuthGuard = class extends AuthGuard("jwt") {
|
|
41
282
|
handleRequest(err, user, _info) {
|
|
42
|
-
if (err != null
|
|
43
|
-
throw err
|
|
283
|
+
if (err != null) {
|
|
284
|
+
throw err;
|
|
285
|
+
}
|
|
286
|
+
if (!user) {
|
|
287
|
+
throw new UnauthorizedException(AuthErrorCode.UNAUTHORIZED);
|
|
44
288
|
}
|
|
45
289
|
return user;
|
|
46
290
|
}
|
|
@@ -54,7 +298,6 @@ var DUMMY_PASSWORD_HASH = "$2a$10$N9qo8uLOickgx2ZMRZoMyeIjZAgcfl7p92ldGxad68LJZd
|
|
|
54
298
|
var DefaultAuthHooks = class {
|
|
55
299
|
async buildJwtPayload(account) {
|
|
56
300
|
return {
|
|
57
|
-
sub: account.id,
|
|
58
301
|
...account.email != null ? { email: account.email } : {},
|
|
59
302
|
...account.username != null ? { username: account.username } : {}
|
|
60
303
|
};
|
|
@@ -107,49 +350,133 @@ function expiresAtFromTtlMs(ttlMs) {
|
|
|
107
350
|
}
|
|
108
351
|
var DEFAULT_EMAIL_VERIFICATION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
109
352
|
var DEFAULT_PASSWORD_RESET_TTL_MS = 15 * 60 * 1e3;
|
|
353
|
+
var JWT_TOKEN_TYPE = {
|
|
354
|
+
ACCESS: "access",
|
|
355
|
+
REFRESH: "refresh"
|
|
356
|
+
};
|
|
357
|
+
function issuerAudienceClaims(options) {
|
|
358
|
+
return {
|
|
359
|
+
...options.jwtIssuer != null ? { iss: options.jwtIssuer } : {},
|
|
360
|
+
...options.jwtAudience != null ? { aud: options.jwtAudience } : {}
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
function buildAccessClaims(hookClaims, sub, pwdAt, options) {
|
|
364
|
+
return {
|
|
365
|
+
...hookClaims,
|
|
366
|
+
sub,
|
|
367
|
+
typ: JWT_TOKEN_TYPE.ACCESS,
|
|
368
|
+
...pwdAt != null ? { pwdAt } : {},
|
|
369
|
+
...issuerAudienceClaims(options)
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
function buildRefreshClaims(sub, pwdAt, options) {
|
|
373
|
+
return {
|
|
374
|
+
sub,
|
|
375
|
+
typ: JWT_TOKEN_TYPE.REFRESH,
|
|
376
|
+
jti: randomUUID(),
|
|
377
|
+
...pwdAt != null ? { pwdAt } : {},
|
|
378
|
+
...issuerAudienceClaims(options)
|
|
379
|
+
};
|
|
380
|
+
}
|
|
381
|
+
function assertIssuerAudience(payload, options, errorCode) {
|
|
382
|
+
if (options.jwtIssuer != null && payload.iss != null && payload.iss !== options.jwtIssuer) {
|
|
383
|
+
throw new UnauthorizedException(errorCode);
|
|
384
|
+
}
|
|
385
|
+
if (options.jwtAudience != null && payload.aud != null && payload.aud !== options.jwtAudience) {
|
|
386
|
+
throw new UnauthorizedException(errorCode);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function assertAccessTokenClaims(payload, options) {
|
|
390
|
+
if (payload.typ != null && payload.typ !== JWT_TOKEN_TYPE.ACCESS) {
|
|
391
|
+
throw new UnauthorizedException(AuthErrorCode.INVALID_CREDENTIALS);
|
|
392
|
+
}
|
|
393
|
+
assertIssuerAudience(payload, options, AuthErrorCode.INVALID_CREDENTIALS);
|
|
394
|
+
}
|
|
395
|
+
function assertRefreshTokenClaims(payload, options) {
|
|
396
|
+
if (payload.typ !== JWT_TOKEN_TYPE.REFRESH) {
|
|
397
|
+
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
398
|
+
}
|
|
399
|
+
if (typeof payload.sub !== "string" || payload.sub.length === 0) {
|
|
400
|
+
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
401
|
+
}
|
|
402
|
+
assertIssuerAudience(payload, options, AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
403
|
+
return payload;
|
|
404
|
+
}
|
|
405
|
+
var HMAC_ALGORITHM = "sha256";
|
|
406
|
+
function hashRefreshTokenValue(refreshToken, secret) {
|
|
407
|
+
return createHmac(HMAC_ALGORITHM, secret).update(refreshToken).digest("hex");
|
|
408
|
+
}
|
|
409
|
+
function compareRefreshTokenValue(refreshToken, storedHash, secret) {
|
|
410
|
+
const computed = hashRefreshTokenValue(refreshToken, secret);
|
|
411
|
+
try {
|
|
412
|
+
const a = Buffer.from(computed, "hex");
|
|
413
|
+
const b = Buffer.from(storedHash, "hex");
|
|
414
|
+
if (a.length !== b.length) return false;
|
|
415
|
+
return timingSafeEqual(a, b);
|
|
416
|
+
} catch {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// src/services/token.service.ts
|
|
110
422
|
var JWT_ALGORITHM = "HS256";
|
|
111
423
|
var TokenService = class {
|
|
112
424
|
constructor(jwtService, options) {
|
|
113
425
|
this.jwtService = jwtService;
|
|
114
426
|
this.options = options;
|
|
115
427
|
}
|
|
116
|
-
|
|
117
|
-
return
|
|
428
|
+
signOptions(secret, expiresIn) {
|
|
429
|
+
return {
|
|
430
|
+
secret,
|
|
431
|
+
expiresIn,
|
|
432
|
+
algorithm: JWT_ALGORITHM,
|
|
433
|
+
...this.options.jwtIssuer != null ? { issuer: this.options.jwtIssuer } : {},
|
|
434
|
+
...this.options.jwtAudience != null ? { audience: this.options.jwtAudience } : {}
|
|
435
|
+
};
|
|
118
436
|
}
|
|
119
|
-
async signTokens(
|
|
437
|
+
async signTokens(accessClaims, refreshClaims) {
|
|
120
438
|
const accessExpiresIn = this.options.expiresIn ?? "1h";
|
|
121
439
|
const refreshExpiresIn = this.options.refreshExpiresIn ?? "7d";
|
|
122
440
|
const [accessToken, refreshToken] = await Promise.all([
|
|
123
441
|
this.jwtService.signAsync(
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
secret: this.options.secret,
|
|
127
|
-
expiresIn: accessExpiresIn,
|
|
128
|
-
algorithm: JWT_ALGORITHM
|
|
129
|
-
}
|
|
442
|
+
accessClaims,
|
|
443
|
+
this.signOptions(this.options.secret, accessExpiresIn)
|
|
130
444
|
),
|
|
131
445
|
this.jwtService.signAsync(
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
secret: this.options.refreshSecret,
|
|
135
|
-
expiresIn: refreshExpiresIn,
|
|
136
|
-
algorithm: JWT_ALGORITHM
|
|
137
|
-
}
|
|
446
|
+
refreshClaims,
|
|
447
|
+
this.signOptions(this.options.refreshSecret, refreshExpiresIn)
|
|
138
448
|
)
|
|
139
449
|
]);
|
|
140
450
|
return { accessToken, refreshToken };
|
|
141
451
|
}
|
|
142
452
|
async verifyRefreshToken(refreshToken) {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
453
|
+
try {
|
|
454
|
+
const payload = await this.jwtService.verifyAsync(
|
|
455
|
+
refreshToken,
|
|
456
|
+
{
|
|
457
|
+
secret: this.options.refreshSecret,
|
|
458
|
+
algorithms: [JWT_ALGORITHM],
|
|
459
|
+
...this.options.jwtIssuer != null ? { issuer: this.options.jwtIssuer } : {},
|
|
460
|
+
...this.options.jwtAudience != null ? { audience: this.options.jwtAudience } : {}
|
|
461
|
+
}
|
|
462
|
+
);
|
|
463
|
+
return assertRefreshTokenClaims(payload, this.options);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
if (error instanceof UnauthorizedException) {
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
469
|
+
}
|
|
147
470
|
}
|
|
148
471
|
async hashRefreshToken(refreshToken) {
|
|
149
|
-
return
|
|
472
|
+
return hashRefreshTokenValue(refreshToken, this.options.refreshSecret);
|
|
150
473
|
}
|
|
151
|
-
async compareRefreshToken(refreshToken,
|
|
152
|
-
return
|
|
474
|
+
async compareRefreshToken(refreshToken, hash2) {
|
|
475
|
+
return compareRefreshTokenValue(
|
|
476
|
+
refreshToken,
|
|
477
|
+
hash2,
|
|
478
|
+
this.options.refreshSecret
|
|
479
|
+
);
|
|
153
480
|
}
|
|
154
481
|
};
|
|
155
482
|
TokenService = __decorateClass([
|
|
@@ -157,6 +484,32 @@ TokenService = __decorateClass([
|
|
|
157
484
|
__decorateParam(0, Inject(JwtService)),
|
|
158
485
|
__decorateParam(1, Inject(AUTH_MODULE_OPTIONS))
|
|
159
486
|
], TokenService);
|
|
487
|
+
function passwordChangedAtMs(account) {
|
|
488
|
+
return account.passwordChangedAt?.getTime();
|
|
489
|
+
}
|
|
490
|
+
function buildPwdAtClaim(account) {
|
|
491
|
+
return passwordChangedAtMs(account);
|
|
492
|
+
}
|
|
493
|
+
function isAccountLocked(account, lockoutEnabled) {
|
|
494
|
+
if (!lockoutEnabled) return false;
|
|
495
|
+
const lockedUntil = "lockedUntil" in account ? account.lockedUntil : void 0;
|
|
496
|
+
if (lockedUntil == null) return false;
|
|
497
|
+
return lockedUntil > /* @__PURE__ */ new Date();
|
|
498
|
+
}
|
|
499
|
+
function assertAccountNotLocked(account, options) {
|
|
500
|
+
if (!isAccountLocked(account, options.features?.accountLockout === true)) {
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_LOCKED);
|
|
504
|
+
}
|
|
505
|
+
function assertPasswordNotStale(payload, account) {
|
|
506
|
+
const changedAt = passwordChangedAtMs(account);
|
|
507
|
+
if (changedAt == null) return;
|
|
508
|
+
const tokenPwdAt = typeof payload.pwdAt === "number" ? payload.pwdAt : void 0;
|
|
509
|
+
if (tokenPwdAt == null || tokenPwdAt < changedAt) {
|
|
510
|
+
throw new UnauthorizedException(AuthErrorCode.PASSWORD_CHANGED);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
160
513
|
|
|
161
514
|
// src/services/auth.service.ts
|
|
162
515
|
var AuthService = class {
|
|
@@ -205,7 +558,7 @@ var AuthService = class {
|
|
|
205
558
|
resolveRegisterIdentifier(input, this.identifierField);
|
|
206
559
|
this.assertRegisterEmailWhenVerificationEnabled(input);
|
|
207
560
|
this.assertPasswordPolicy(dto.password);
|
|
208
|
-
const passwordHash = await
|
|
561
|
+
const passwordHash = await bcrypt.hash(dto.password, this.bcryptRounds);
|
|
209
562
|
try {
|
|
210
563
|
const account = await this.authRepository.create({
|
|
211
564
|
...input,
|
|
@@ -229,11 +582,8 @@ var AuthService = class {
|
|
|
229
582
|
const account = await this.authRepository.findByIdentifierWithSecrets(
|
|
230
583
|
identifier
|
|
231
584
|
);
|
|
232
|
-
if (account != null) {
|
|
233
|
-
this.assertAccountNotLocked(account);
|
|
234
|
-
}
|
|
235
585
|
const passwordHash = account?.passwordHash ?? DUMMY_PASSWORD_HASH;
|
|
236
|
-
const passwordMatches = await
|
|
586
|
+
const passwordMatches = await bcrypt.compare(dto.password, passwordHash);
|
|
237
587
|
if (account?.passwordHash == null || !passwordMatches) {
|
|
238
588
|
if (account != null && this.accountLockoutEnabled) {
|
|
239
589
|
await this.authRepository.recordLoginFailure(
|
|
@@ -243,6 +593,7 @@ var AuthService = class {
|
|
|
243
593
|
}
|
|
244
594
|
throw new UnauthorizedException(AuthErrorCode.INVALID_CREDENTIALS);
|
|
245
595
|
}
|
|
596
|
+
assertAccountNotLocked(account, this.options);
|
|
246
597
|
this.assertAccountActive(account);
|
|
247
598
|
await this.authRepository.recordLoginSuccess(account.id);
|
|
248
599
|
return this.issueTokens(account);
|
|
@@ -251,14 +602,25 @@ var AuthService = class {
|
|
|
251
602
|
let payload;
|
|
252
603
|
try {
|
|
253
604
|
payload = await this.tokenService.verifyRefreshToken(refreshToken);
|
|
254
|
-
} catch {
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (error instanceof UnauthorizedException) {
|
|
607
|
+
throw error;
|
|
608
|
+
}
|
|
255
609
|
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
256
610
|
}
|
|
257
611
|
const account = await this.authRepository.findByIdWithSecrets(payload.sub);
|
|
258
612
|
if (account == null) {
|
|
259
613
|
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
260
614
|
}
|
|
615
|
+
assertPasswordNotStale(
|
|
616
|
+
{
|
|
617
|
+
sub: payload.sub,
|
|
618
|
+
...payload.pwdAt != null ? { pwdAt: payload.pwdAt } : {}
|
|
619
|
+
},
|
|
620
|
+
account
|
|
621
|
+
);
|
|
261
622
|
this.assertAccountActive(account);
|
|
623
|
+
assertAccountNotLocked(account, this.options);
|
|
262
624
|
if (this.rotateRefreshToken) {
|
|
263
625
|
if (account.refreshTokenHash == null) {
|
|
264
626
|
throw new UnauthorizedException(AuthErrorCode.INVALID_REFRESH_TOKEN);
|
|
@@ -271,6 +633,9 @@ var AuthService = class {
|
|
|
271
633
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
272
634
|
throw new UnauthorizedException(AuthErrorCode.REFRESH_TOKEN_REUSE);
|
|
273
635
|
}
|
|
636
|
+
return this.issueTokens(account, {
|
|
637
|
+
expectedRefreshHash: account.refreshTokenHash
|
|
638
|
+
});
|
|
274
639
|
}
|
|
275
640
|
return this.issueTokens(account);
|
|
276
641
|
}
|
|
@@ -281,7 +646,7 @@ var AuthService = class {
|
|
|
281
646
|
async me(authId) {
|
|
282
647
|
const account = await this.authRepository.findById(authId);
|
|
283
648
|
if (account == null) {
|
|
284
|
-
throw new UnauthorizedException(
|
|
649
|
+
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_NOT_FOUND);
|
|
285
650
|
}
|
|
286
651
|
if (this.hooks.enrichMe != null) {
|
|
287
652
|
return this.hooks.enrichMe(account);
|
|
@@ -322,18 +687,37 @@ var AuthService = class {
|
|
|
322
687
|
throw new BadRequestException(AuthErrorCode.TOKEN_INVALID_OR_EXPIRED);
|
|
323
688
|
}
|
|
324
689
|
this.assertPasswordPolicy(newPassword);
|
|
325
|
-
const
|
|
690
|
+
const samePassword = await bcrypt.compare(
|
|
691
|
+
newPassword,
|
|
692
|
+
account.passwordHash
|
|
693
|
+
);
|
|
694
|
+
if (samePassword) {
|
|
695
|
+
throw new BadRequestException(AuthErrorCode.PASSWORD_UNCHANGED);
|
|
696
|
+
}
|
|
697
|
+
const passwordHash = await bcrypt.hash(newPassword, this.bcryptRounds);
|
|
326
698
|
await this.authRepository.updatePasswordHash(account.id, passwordHash);
|
|
327
699
|
await this.authRepository.clearResetToken(account.id);
|
|
328
700
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
329
701
|
return { reset: true };
|
|
330
702
|
}
|
|
703
|
+
async resendVerification(email) {
|
|
704
|
+
this.assertEmailVerificationEnabled();
|
|
705
|
+
this.assertEmailHookWhenVerificationEnabled();
|
|
706
|
+
const normalizedEmail = normalizeIdentifier(email);
|
|
707
|
+
const account = await this.authRepository.findUnverifiedByEmail(
|
|
708
|
+
normalizedEmail
|
|
709
|
+
);
|
|
710
|
+
if (account != null && !account.disabled) {
|
|
711
|
+
await this.sendVerificationEmail(account);
|
|
712
|
+
}
|
|
713
|
+
return { sent: true };
|
|
714
|
+
}
|
|
331
715
|
async changePassword(authId, currentPassword, newPassword) {
|
|
332
716
|
const account = await this.authRepository.findByIdWithSecrets(authId);
|
|
333
717
|
if (account?.passwordHash == null) {
|
|
334
718
|
throw new UnauthorizedException(AuthErrorCode.INVALID_CURRENT_PASSWORD);
|
|
335
719
|
}
|
|
336
|
-
const currentMatches = await
|
|
720
|
+
const currentMatches = await bcrypt.compare(
|
|
337
721
|
currentPassword,
|
|
338
722
|
account.passwordHash
|
|
339
723
|
);
|
|
@@ -344,19 +728,11 @@ var AuthService = class {
|
|
|
344
728
|
throw new BadRequestException(AuthErrorCode.PASSWORD_UNCHANGED);
|
|
345
729
|
}
|
|
346
730
|
this.assertPasswordPolicy(newPassword);
|
|
347
|
-
const passwordHash = await
|
|
731
|
+
const passwordHash = await bcrypt.hash(newPassword, this.bcryptRounds);
|
|
348
732
|
await this.authRepository.updatePasswordHash(account.id, passwordHash);
|
|
349
733
|
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
350
734
|
return { changed: true };
|
|
351
735
|
}
|
|
352
|
-
assertAccountNotLocked(account) {
|
|
353
|
-
if (!this.accountLockoutEnabled || account.lockedUntil == null) {
|
|
354
|
-
return;
|
|
355
|
-
}
|
|
356
|
-
if (account.lockedUntil > /* @__PURE__ */ new Date()) {
|
|
357
|
-
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_LOCKED);
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
736
|
assertAccountActive(account) {
|
|
361
737
|
if (account.disabled) {
|
|
362
738
|
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_DISABLED);
|
|
@@ -370,20 +746,33 @@ var AuthService = class {
|
|
|
370
746
|
assertPasswordComplexity(password);
|
|
371
747
|
}
|
|
372
748
|
}
|
|
373
|
-
async issueTokens(account) {
|
|
374
|
-
const
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
749
|
+
async issueTokens(account, rotation) {
|
|
750
|
+
const hookPayload = await this.hooks.buildJwtPayload(account);
|
|
751
|
+
const pwdAt = buildPwdAtClaim(account);
|
|
752
|
+
const tokens = await this.tokenService.signTokens(
|
|
753
|
+
buildAccessClaims(hookPayload, account.id, pwdAt, this.options),
|
|
754
|
+
buildRefreshClaims(account.id, pwdAt, this.options)
|
|
755
|
+
);
|
|
379
756
|
if (this.rotateRefreshToken) {
|
|
380
757
|
const refreshTokenHash = await this.tokenService.hashRefreshToken(
|
|
381
758
|
tokens.refreshToken
|
|
382
759
|
);
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
760
|
+
if (rotation?.expectedRefreshHash != null) {
|
|
761
|
+
const swapped = await this.authRepository.rotateRefreshTokenHashIfMatch(
|
|
762
|
+
account.id,
|
|
763
|
+
rotation.expectedRefreshHash,
|
|
764
|
+
refreshTokenHash
|
|
765
|
+
);
|
|
766
|
+
if (!swapped) {
|
|
767
|
+
await this.authRepository.updateRefreshTokenHash(account.id, null);
|
|
768
|
+
throw new UnauthorizedException(AuthErrorCode.REFRESH_TOKEN_REUSE);
|
|
769
|
+
}
|
|
770
|
+
} else {
|
|
771
|
+
await this.authRepository.updateRefreshTokenHash(
|
|
772
|
+
account.id,
|
|
773
|
+
refreshTokenHash
|
|
774
|
+
);
|
|
775
|
+
}
|
|
387
776
|
}
|
|
388
777
|
await this.hooks.onAfterLogin?.(account);
|
|
389
778
|
return tokens;
|
|
@@ -453,6 +842,21 @@ AuthService = __decorateClass([
|
|
|
453
842
|
__decorateParam(2, Inject(AUTH_HOOKS)),
|
|
454
843
|
__decorateParam(3, Inject(TokenService))
|
|
455
844
|
], AuthService);
|
|
845
|
+
function ApiAuthUnauthorizedResponse(...codes) {
|
|
846
|
+
const messageEnum = codes.length > 0 ? codes : Object.values(AuthErrorCode);
|
|
847
|
+
return applyDecorators(
|
|
848
|
+
ApiUnauthorizedResponse({
|
|
849
|
+
description: "Unauthorized \u2014 `message` is an `AuthErrorCode` value",
|
|
850
|
+
schema: {
|
|
851
|
+
type: "object",
|
|
852
|
+
properties: {
|
|
853
|
+
statusCode: { type: "number", example: 401 },
|
|
854
|
+
message: { type: "string", enum: messageEnum }
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
})
|
|
858
|
+
);
|
|
859
|
+
}
|
|
456
860
|
|
|
457
861
|
// src/controllers/auth.controller.ts
|
|
458
862
|
var AuthController = class {
|
|
@@ -477,6 +881,9 @@ var AuthController = class {
|
|
|
477
881
|
verifyEmail(dto) {
|
|
478
882
|
return this.authService.verifyEmail(dto.token);
|
|
479
883
|
}
|
|
884
|
+
resendVerification(dto) {
|
|
885
|
+
return this.authService.resendVerification(dto.email);
|
|
886
|
+
}
|
|
480
887
|
forgotPassword(dto) {
|
|
481
888
|
return this.authService.forgotPassword(dto.email);
|
|
482
889
|
}
|
|
@@ -493,52 +900,118 @@ var AuthController = class {
|
|
|
493
900
|
};
|
|
494
901
|
__decorateClass([
|
|
495
902
|
Post("register"),
|
|
903
|
+
ApiOperation({ summary: "Register a new account" }),
|
|
904
|
+
ApiResponse({ status: 201, type: RegisterAckDto }),
|
|
496
905
|
__decorateParam(0, Body())
|
|
497
906
|
], AuthController.prototype, "register", 1);
|
|
498
907
|
__decorateClass([
|
|
499
908
|
Post("login"),
|
|
909
|
+
HttpCode(HttpStatus.OK),
|
|
910
|
+
ApiOperation({ summary: "Login and receive JWT tokens" }),
|
|
911
|
+
ApiResponse({ status: 200, type: AuthTokensDto }),
|
|
912
|
+
ApiAuthUnauthorizedResponse(
|
|
913
|
+
AuthErrorCode.INVALID_CREDENTIALS,
|
|
914
|
+
AuthErrorCode.ACCOUNT_LOCKED,
|
|
915
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
916
|
+
AuthErrorCode.ACCOUNT_DISABLED
|
|
917
|
+
),
|
|
500
918
|
__decorateParam(0, Body())
|
|
501
919
|
], AuthController.prototype, "login", 1);
|
|
502
920
|
__decorateClass([
|
|
503
921
|
Post("refresh"),
|
|
504
922
|
HttpCode(HttpStatus.OK),
|
|
923
|
+
ApiOperation({ summary: "Refresh access token using refresh token" }),
|
|
924
|
+
ApiResponse({ status: 200, type: AuthTokensDto }),
|
|
925
|
+
ApiAuthUnauthorizedResponse(
|
|
926
|
+
AuthErrorCode.INVALID_REFRESH_TOKEN,
|
|
927
|
+
AuthErrorCode.REFRESH_TOKEN_REUSE,
|
|
928
|
+
AuthErrorCode.PASSWORD_CHANGED,
|
|
929
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
930
|
+
AuthErrorCode.ACCOUNT_LOCKED
|
|
931
|
+
),
|
|
505
932
|
__decorateParam(0, Body())
|
|
506
933
|
], AuthController.prototype, "refresh", 1);
|
|
507
934
|
__decorateClass([
|
|
508
935
|
Post("logout"),
|
|
509
936
|
UseGuards(JwtAuthGuard),
|
|
937
|
+
ApiBearerAuth("access-token"),
|
|
510
938
|
HttpCode(HttpStatus.OK),
|
|
939
|
+
ApiOperation({ summary: "Logout and revoke refresh token" }),
|
|
940
|
+
ApiResponse({ status: 200, schema: { example: { loggedOut: true } } }),
|
|
941
|
+
ApiAuthUnauthorizedResponse(AuthErrorCode.UNAUTHORIZED),
|
|
511
942
|
__decorateParam(0, CurrentUser())
|
|
512
943
|
], AuthController.prototype, "logout", 1);
|
|
513
944
|
__decorateClass([
|
|
514
945
|
Get("me"),
|
|
515
946
|
UseGuards(JwtAuthGuard),
|
|
947
|
+
ApiBearerAuth("access-token"),
|
|
948
|
+
ApiOperation({ summary: "Get current authenticated user profile" }),
|
|
949
|
+
ApiResponse({ status: 200, type: MeResponseDto }),
|
|
950
|
+
ApiAuthUnauthorizedResponse(
|
|
951
|
+
AuthErrorCode.UNAUTHORIZED,
|
|
952
|
+
AuthErrorCode.ACCOUNT_NOT_FOUND,
|
|
953
|
+
AuthErrorCode.PASSWORD_CHANGED,
|
|
954
|
+
AuthErrorCode.EMAIL_NOT_VERIFIED,
|
|
955
|
+
AuthErrorCode.ACCOUNT_LOCKED
|
|
956
|
+
),
|
|
516
957
|
__decorateParam(0, CurrentUser())
|
|
517
958
|
], AuthController.prototype, "me", 1);
|
|
518
959
|
__decorateClass([
|
|
519
960
|
Post("verify-email"),
|
|
520
961
|
HttpCode(HttpStatus.OK),
|
|
962
|
+
ApiOperation({
|
|
963
|
+
summary: "Verify email with token (requires emailVerification feature)"
|
|
964
|
+
}),
|
|
965
|
+
ApiResponse({ status: 200, schema: { example: { verified: true } } }),
|
|
966
|
+
ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
521
967
|
__decorateParam(0, Body())
|
|
522
968
|
], AuthController.prototype, "verifyEmail", 1);
|
|
969
|
+
__decorateClass([
|
|
970
|
+
Post("resend-verification"),
|
|
971
|
+
HttpCode(HttpStatus.OK),
|
|
972
|
+
ApiOperation({
|
|
973
|
+
summary: "Resend verification email (requires emailVerification feature)"
|
|
974
|
+
}),
|
|
975
|
+
ApiResponse({ status: 200, schema: { example: { sent: true } } }),
|
|
976
|
+
ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
977
|
+
__decorateParam(0, Body())
|
|
978
|
+
], AuthController.prototype, "resendVerification", 1);
|
|
523
979
|
__decorateClass([
|
|
524
980
|
Post("forgot-password"),
|
|
525
981
|
HttpCode(HttpStatus.OK),
|
|
982
|
+
ApiOperation({
|
|
983
|
+
summary: "Request password reset email (requires passwordReset feature)"
|
|
984
|
+
}),
|
|
985
|
+
ApiResponse({ status: 200, schema: { example: { sent: true } } }),
|
|
986
|
+
ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
526
987
|
__decorateParam(0, Body())
|
|
527
988
|
], AuthController.prototype, "forgotPassword", 1);
|
|
528
989
|
__decorateClass([
|
|
529
990
|
Post("reset-password"),
|
|
530
991
|
HttpCode(HttpStatus.OK),
|
|
992
|
+
ApiOperation({
|
|
993
|
+
summary: "Reset password with token (requires passwordReset feature)"
|
|
994
|
+
}),
|
|
995
|
+
ApiResponse({ status: 200, schema: { example: { reset: true } } }),
|
|
996
|
+
ApiResponse({ status: 404, description: "Feature disabled" }),
|
|
531
997
|
__decorateParam(0, Body())
|
|
532
998
|
], AuthController.prototype, "resetPassword", 1);
|
|
533
999
|
__decorateClass([
|
|
534
1000
|
Post("change-password"),
|
|
535
1001
|
UseGuards(JwtAuthGuard),
|
|
1002
|
+
ApiBearerAuth("access-token"),
|
|
536
1003
|
HttpCode(HttpStatus.OK),
|
|
1004
|
+
ApiOperation({ summary: "Change password for authenticated user" }),
|
|
1005
|
+
ApiResponse({ status: 200, schema: { example: { changed: true } } }),
|
|
1006
|
+
ApiAuthUnauthorizedResponse(
|
|
1007
|
+
AuthErrorCode.UNAUTHORIZED,
|
|
1008
|
+
AuthErrorCode.INVALID_CURRENT_PASSWORD
|
|
1009
|
+
),
|
|
537
1010
|
__decorateParam(0, CurrentUser()),
|
|
538
1011
|
__decorateParam(1, Body())
|
|
539
1012
|
], AuthController.prototype, "changePassword", 1);
|
|
540
1013
|
AuthController = __decorateClass([
|
|
541
|
-
|
|
1014
|
+
ApiTags("auth"),
|
|
542
1015
|
__decorateParam(0, Inject(AuthService))
|
|
543
1016
|
], AuthController);
|
|
544
1017
|
|
|
@@ -560,19 +1033,55 @@ var JwtStrategy = class extends PassportStrategy(Strategy) {
|
|
|
560
1033
|
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
|
|
561
1034
|
ignoreExpiration: false,
|
|
562
1035
|
secretOrKey: options.secret,
|
|
563
|
-
algorithms: ["HS256"]
|
|
1036
|
+
algorithms: ["HS256"],
|
|
1037
|
+
...options.jwtIssuer != null ? { issuer: options.jwtIssuer } : {},
|
|
1038
|
+
...options.jwtAudience != null ? { audience: options.jwtAudience } : {}
|
|
564
1039
|
});
|
|
565
1040
|
this.options = options;
|
|
566
1041
|
this.authRepository = authRepository;
|
|
1042
|
+
this.validationCache = /* @__PURE__ */ new Map();
|
|
567
1043
|
}
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
1044
|
+
get cacheTtlMs() {
|
|
1045
|
+
return this.options.jwtValidationCacheTtlMs ?? 0;
|
|
1046
|
+
}
|
|
1047
|
+
getCachedAccount(sub) {
|
|
1048
|
+
const cached = this.validationCache.get(sub);
|
|
1049
|
+
if (cached == null || cached.expiresAt <= Date.now()) {
|
|
1050
|
+
if (cached != null) this.validationCache.delete(sub);
|
|
1051
|
+
return null;
|
|
1052
|
+
}
|
|
1053
|
+
return cached.account;
|
|
1054
|
+
}
|
|
1055
|
+
cacheAccount(sub, account) {
|
|
1056
|
+
if (this.cacheTtlMs <= 0) return;
|
|
1057
|
+
this.validationCache.set(sub, {
|
|
1058
|
+
account,
|
|
1059
|
+
expiresAt: Date.now() + this.cacheTtlMs
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
assertAccountActive(account, payload) {
|
|
1063
|
+
if (account.disabled) {
|
|
1064
|
+
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_DISABLED);
|
|
572
1065
|
}
|
|
1066
|
+
assertAccountNotLocked(account, this.options);
|
|
573
1067
|
if (this.options.features?.emailVerification === true && !account.emailVerified) {
|
|
574
1068
|
throw new UnauthorizedException(AuthErrorCode.EMAIL_NOT_VERIFIED);
|
|
575
1069
|
}
|
|
1070
|
+
assertPasswordNotStale(payload, account);
|
|
1071
|
+
}
|
|
1072
|
+
async validate(payload) {
|
|
1073
|
+
assertAccessTokenClaims(payload, this.options);
|
|
1074
|
+
const cached = this.getCachedAccount(payload.sub);
|
|
1075
|
+
if (cached != null) {
|
|
1076
|
+
this.assertAccountActive(cached, payload);
|
|
1077
|
+
return payload;
|
|
1078
|
+
}
|
|
1079
|
+
const account = await this.authRepository.findById(payload.sub);
|
|
1080
|
+
if (account == null) {
|
|
1081
|
+
throw new UnauthorizedException(AuthErrorCode.ACCOUNT_NOT_FOUND);
|
|
1082
|
+
}
|
|
1083
|
+
this.assertAccountActive(account, payload);
|
|
1084
|
+
this.cacheAccount(payload.sub, account);
|
|
576
1085
|
return payload;
|
|
577
1086
|
}
|
|
578
1087
|
};
|
|
@@ -581,8 +1090,6 @@ JwtStrategy = __decorateClass([
|
|
|
581
1090
|
__decorateParam(0, Inject(AUTH_MODULE_OPTIONS)),
|
|
582
1091
|
__decorateParam(1, Inject(AUTH_REPOSITORY))
|
|
583
1092
|
], JwtStrategy);
|
|
584
|
-
|
|
585
|
-
// src/utils/hooks-provider.util.ts
|
|
586
1093
|
function createHooksProvider(options) {
|
|
587
1094
|
if (options.hooksProvider != null) {
|
|
588
1095
|
return options.hooksProvider;
|
|
@@ -590,7 +1097,8 @@ function createHooksProvider(options) {
|
|
|
590
1097
|
const HooksClass = options.hooks ?? DefaultAuthHooks;
|
|
591
1098
|
return {
|
|
592
1099
|
provide: AUTH_HOOKS,
|
|
593
|
-
|
|
1100
|
+
inject: [ModuleRef],
|
|
1101
|
+
useFactory: (moduleRef) => moduleRef.create(HooksClass)
|
|
594
1102
|
};
|
|
595
1103
|
}
|
|
596
1104
|
|
|
@@ -630,6 +1138,17 @@ function validateAuthModuleOptions(options) {
|
|
|
630
1138
|
"AuthModule: passwordResetTokenTtlMs must be at least 60000 (1 minute)"
|
|
631
1139
|
);
|
|
632
1140
|
}
|
|
1141
|
+
if (options.features?.refreshTokenRotation === false) {
|
|
1142
|
+
console.warn(
|
|
1143
|
+
"[aranza-auth] features.refreshTokenRotation is false \u2014 refresh tokens are stateless until JWT expiry; stolen tokens cannot be revoked server-side."
|
|
1144
|
+
);
|
|
1145
|
+
}
|
|
1146
|
+
const cacheTtl = options.jwtValidationCacheTtlMs ?? 0;
|
|
1147
|
+
if (cacheTtl < 0 || cacheTtl > 3e5) {
|
|
1148
|
+
throw new Error(
|
|
1149
|
+
"AuthModule: jwtValidationCacheTtlMs must be between 0 and 300000 (5 minutes)"
|
|
1150
|
+
);
|
|
1151
|
+
}
|
|
633
1152
|
}
|
|
634
1153
|
|
|
635
1154
|
// src/auth.module.ts
|
|
@@ -647,15 +1166,33 @@ function createCoreProviders(options) {
|
|
|
647
1166
|
JwtAuthGuard
|
|
648
1167
|
];
|
|
649
1168
|
}
|
|
650
|
-
function
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1169
|
+
function createAsyncProviders(options) {
|
|
1170
|
+
return [
|
|
1171
|
+
{
|
|
1172
|
+
provide: AUTH_MODULE_OPTIONS,
|
|
1173
|
+
inject: options.inject ?? [],
|
|
1174
|
+
useFactory: async (...args) => {
|
|
1175
|
+
const config = await options.useFactory(...args);
|
|
1176
|
+
validateAuthModuleOptions(config);
|
|
1177
|
+
return config;
|
|
1178
|
+
}
|
|
1179
|
+
},
|
|
1180
|
+
createHooksProvider(options),
|
|
1181
|
+
AuthService,
|
|
1182
|
+
TokenService,
|
|
1183
|
+
JwtStrategy,
|
|
1184
|
+
JwtAuthGuard
|
|
1185
|
+
];
|
|
1186
|
+
}
|
|
1187
|
+
function jwtModuleOptions(opts) {
|
|
655
1188
|
return {
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
1189
|
+
secret: opts.secret,
|
|
1190
|
+
signOptions: {
|
|
1191
|
+
expiresIn: opts.expiresIn ?? "1h",
|
|
1192
|
+
algorithm: "HS256",
|
|
1193
|
+
...opts.jwtIssuer != null ? { issuer: opts.jwtIssuer } : {},
|
|
1194
|
+
...opts.jwtAudience != null ? { audience: opts.jwtAudience } : {}
|
|
1195
|
+
}
|
|
659
1196
|
};
|
|
660
1197
|
}
|
|
661
1198
|
function createAuthImports() {
|
|
@@ -663,13 +1200,7 @@ function createAuthImports() {
|
|
|
663
1200
|
PassportModule.register({ defaultStrategy: "jwt" }),
|
|
664
1201
|
JwtModule.registerAsync({
|
|
665
1202
|
inject: [AUTH_MODULE_OPTIONS],
|
|
666
|
-
useFactory: (opts) => (
|
|
667
|
-
secret: opts.secret,
|
|
668
|
-
signOptions: {
|
|
669
|
-
expiresIn: opts.expiresIn ?? "1h",
|
|
670
|
-
algorithm: "HS256"
|
|
671
|
-
}
|
|
672
|
-
})
|
|
1203
|
+
useFactory: (opts) => jwtModuleOptions(opts)
|
|
673
1204
|
})
|
|
674
1205
|
];
|
|
675
1206
|
}
|
|
@@ -680,24 +1211,6 @@ function mergeImports(userImports) {
|
|
|
680
1211
|
}
|
|
681
1212
|
return merged;
|
|
682
1213
|
}
|
|
683
|
-
function createAsyncProviders(options) {
|
|
684
|
-
return [
|
|
685
|
-
{
|
|
686
|
-
provide: AUTH_MODULE_OPTIONS,
|
|
687
|
-
inject: options.inject ?? [],
|
|
688
|
-
useFactory: async (...args) => {
|
|
689
|
-
const config = await options.useFactory(...args);
|
|
690
|
-
validateAuthModuleOptions(config);
|
|
691
|
-
return config;
|
|
692
|
-
}
|
|
693
|
-
},
|
|
694
|
-
createAsyncHooksProvider(options),
|
|
695
|
-
AuthService,
|
|
696
|
-
TokenService,
|
|
697
|
-
JwtStrategy,
|
|
698
|
-
JwtAuthGuard
|
|
699
|
-
];
|
|
700
|
-
}
|
|
701
1214
|
var AuthModule = class {
|
|
702
1215
|
static forRoot(options) {
|
|
703
1216
|
const routePrefix = options.routePrefix ?? "auth";
|
|
@@ -742,90 +1255,6 @@ AuthModule = __decorateClass([
|
|
|
742
1255
|
Module({})
|
|
743
1256
|
], AuthModule);
|
|
744
1257
|
|
|
745
|
-
|
|
746
|
-
var AuthTokensDto = class {
|
|
747
|
-
};
|
|
748
|
-
var ChangePasswordDto = class {
|
|
749
|
-
};
|
|
750
|
-
__decorateClass([
|
|
751
|
-
IsString(),
|
|
752
|
-
IsNotEmpty()
|
|
753
|
-
], ChangePasswordDto.prototype, "currentPassword", 2);
|
|
754
|
-
__decorateClass([
|
|
755
|
-
IsString(),
|
|
756
|
-
IsNotEmpty(),
|
|
757
|
-
Length(8, 128)
|
|
758
|
-
], ChangePasswordDto.prototype, "newPassword", 2);
|
|
759
|
-
var ForgotPasswordDto = class {
|
|
760
|
-
};
|
|
761
|
-
__decorateClass([
|
|
762
|
-
IsEmail()
|
|
763
|
-
], ForgotPasswordDto.prototype, "email", 2);
|
|
764
|
-
var LoginDto = class {
|
|
765
|
-
};
|
|
766
|
-
__decorateClass([
|
|
767
|
-
IsOptional(),
|
|
768
|
-
ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
769
|
-
IsEmail(),
|
|
770
|
-
Length(3, 255)
|
|
771
|
-
], LoginDto.prototype, "email", 2);
|
|
772
|
-
__decorateClass([
|
|
773
|
-
IsOptional(),
|
|
774
|
-
IsString(),
|
|
775
|
-
Length(3, 50)
|
|
776
|
-
], LoginDto.prototype, "username", 2);
|
|
777
|
-
__decorateClass([
|
|
778
|
-
IsString(),
|
|
779
|
-
IsNotEmpty(),
|
|
780
|
-
Length(8, 128)
|
|
781
|
-
], LoginDto.prototype, "password", 2);
|
|
782
|
-
var RefreshTokenDto = class {
|
|
783
|
-
};
|
|
784
|
-
__decorateClass([
|
|
785
|
-
IsString(),
|
|
786
|
-
IsNotEmpty()
|
|
787
|
-
], RefreshTokenDto.prototype, "refreshToken", 2);
|
|
788
|
-
|
|
789
|
-
// src/dto/register-ack.dto.ts
|
|
790
|
-
var RegisterAckDto = class {
|
|
791
|
-
};
|
|
792
|
-
var RegisterDto = class {
|
|
793
|
-
};
|
|
794
|
-
__decorateClass([
|
|
795
|
-
IsOptional(),
|
|
796
|
-
ValidateIf((dto) => dto.email != null && dto.email.trim() !== ""),
|
|
797
|
-
IsEmail(),
|
|
798
|
-
Length(3, 255)
|
|
799
|
-
], RegisterDto.prototype, "email", 2);
|
|
800
|
-
__decorateClass([
|
|
801
|
-
IsOptional(),
|
|
802
|
-
IsString(),
|
|
803
|
-
Length(3, 50),
|
|
804
|
-
Matches(/^[a-zA-Z0-9._-]+$/)
|
|
805
|
-
], RegisterDto.prototype, "username", 2);
|
|
806
|
-
__decorateClass([
|
|
807
|
-
IsString(),
|
|
808
|
-
IsNotEmpty(),
|
|
809
|
-
Length(8, 128)
|
|
810
|
-
], RegisterDto.prototype, "password", 2);
|
|
811
|
-
var ResetPasswordDto = class {
|
|
812
|
-
};
|
|
813
|
-
__decorateClass([
|
|
814
|
-
IsString(),
|
|
815
|
-
IsNotEmpty()
|
|
816
|
-
], ResetPasswordDto.prototype, "token", 2);
|
|
817
|
-
__decorateClass([
|
|
818
|
-
IsString(),
|
|
819
|
-
IsNotEmpty(),
|
|
820
|
-
Length(8, 128)
|
|
821
|
-
], ResetPasswordDto.prototype, "newPassword", 2);
|
|
822
|
-
var VerifyEmailDto = class {
|
|
823
|
-
};
|
|
824
|
-
__decorateClass([
|
|
825
|
-
IsString(),
|
|
826
|
-
IsNotEmpty()
|
|
827
|
-
], VerifyEmailDto.prototype, "token", 2);
|
|
828
|
-
|
|
829
|
-
export { AUTH_RATE_LIMIT_PRESETS, AuthErrorCode, AuthModule, AuthService, AuthTokensDto, ChangePasswordDto, CurrentUser, DefaultAuthHooks, ForgotPasswordDto, JwtAuthGuard, LoginDto, RefreshTokenDto, RegisterAckDto, RegisterDto, ResetPasswordDto, TokenService, VerifyEmailDto };
|
|
1258
|
+
export { AUTH_RATE_LIMIT_PRESETS, AUTH_RATE_LIMIT_ROUTES, AuthErrorCode, AuthModule, AuthService, AuthTokensDto, ChangePasswordDto, CurrentUser, DefaultAuthHooks, ForgotPasswordDto, JwtAuthGuard, LoginDto, MeResponseDto, RefreshTokenDto, RegisterAckDto, RegisterDto, ResendVerificationDto, ResetPasswordDto, TokenService, VerifyEmailDto, buildClearRefreshTokenCookie, buildRefreshTokenCookie, setupAuthSwagger };
|
|
830
1259
|
//# sourceMappingURL=index.js.map
|
|
831
1260
|
//# sourceMappingURL=index.js.map
|