@nabhiraj/nab-auth 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +362 -2
- package/package.json +45 -4
- package/src/authenticationLogic/config/createAuthConfig.js +94 -0
- package/src/authenticationLogic/config/defaultAuthConfig.js +59 -0
- package/src/authenticationLogic/controllers/authController.js +516 -0
- package/src/authenticationLogic/createAuthenticationLibrary.js +43 -0
- package/src/authenticationLogic/index.js +3 -0
- package/src/authenticationLogic/middleware/jwtMiddleware.js +35 -0
- package/src/authenticationLogic/repositories/userRepository.js +281 -0
- package/src/authenticationLogic/routes/authRoutes.js +40 -0
- package/src/authenticationLogic/services/jwtKeyProvider.js +108 -0
- package/src/authenticationLogic/services/tokenService.js +86 -0
- package/src/authenticationLogic/services/verificationCodeService.js +26 -0
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
import argon2 from "argon2";
|
|
2
|
+
import { parseCookie } from "cookie";
|
|
3
|
+
import { normalizeEmail } from "../repositories/userRepository.js";
|
|
4
|
+
import {
|
|
5
|
+
generateNumericVerificationCode,
|
|
6
|
+
hashVerificationCode,
|
|
7
|
+
verifyVerificationCode
|
|
8
|
+
} from "../services/verificationCodeService.js";
|
|
9
|
+
|
|
10
|
+
const dummyPasswordHash =
|
|
11
|
+
"$argon2id$v=19$m=19456,p=1,t=2$4q0Ld3WLtjFp/T4xsM65OA$6fForJ99UdqrscIWMKksrwr/rNpl/bZ3qZc8v/p1YlI";
|
|
12
|
+
|
|
13
|
+
export function createAuthController({
|
|
14
|
+
config,
|
|
15
|
+
repository,
|
|
16
|
+
tokenService,
|
|
17
|
+
payloadFactory,
|
|
18
|
+
options = {}
|
|
19
|
+
}) {
|
|
20
|
+
if (typeof payloadFactory !== "function") {
|
|
21
|
+
throw new TypeError("createAuthController expects a payload function");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const { sendRegistrationCode, sendTwoFactorCode } = options;
|
|
25
|
+
validateCallback(sendRegistrationCode, "sendRegistrationCode");
|
|
26
|
+
validateCallback(sendTwoFactorCode, "sendTwoFactorCode");
|
|
27
|
+
|
|
28
|
+
const findTargetUser = async (body) => {
|
|
29
|
+
const userId = body?.user_id;
|
|
30
|
+
const email = body?.email;
|
|
31
|
+
|
|
32
|
+
if (
|
|
33
|
+
(userId !== undefined && email !== undefined) ||
|
|
34
|
+
(userId === undefined && email === undefined)
|
|
35
|
+
) {
|
|
36
|
+
return { error: "Provide exactly one of user_id or email" };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (userId !== undefined) {
|
|
40
|
+
if (typeof userId !== "string" || !userId) {
|
|
41
|
+
return { error: "user_id must be a non-empty string" };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { user: await repository.findUserByUserId(userId) };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (typeof email !== "string" || !email.trim()) {
|
|
48
|
+
return { error: "email must be a non-empty string" };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { user: await repository.findUserByEmail(normalizeEmail(email)) };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const normalizeRoles = (roles, allowEmpty = false) => {
|
|
55
|
+
if (!Array.isArray(roles) || (!allowEmpty && roles.length === 0)) {
|
|
56
|
+
return { error: "roles must be an array of strings" };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const normalizedRoles = roles.map((role) =>
|
|
60
|
+
typeof role === "string" ? role.trim() : role
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
if (normalizedRoles.some((role) => typeof role !== "string" || !role)) {
|
|
64
|
+
return { error: "roles must be an array of strings" };
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return { roles: [...new Set(normalizedRoles)] };
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const roleResponse = (user) => ({
|
|
71
|
+
user_id: user.user_id,
|
|
72
|
+
email: user.email,
|
|
73
|
+
nab_role: user.nab_role
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
async function createPotentialUser(req, res, next) {
|
|
77
|
+
try {
|
|
78
|
+
const email = req.body?.email;
|
|
79
|
+
|
|
80
|
+
if (typeof email !== "string" || !email.trim()) {
|
|
81
|
+
return res.status(400).json({ message: "email is required" });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const normalizedEmail = normalizeEmail(email);
|
|
85
|
+
|
|
86
|
+
if (await repository.findUserByEmail(normalizedEmail)) {
|
|
87
|
+
await repository.deletePotentialUserByEmail(normalizedEmail);
|
|
88
|
+
return res.status(409).json({ message: "User already exists" });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
requireDeliveryCallback(
|
|
92
|
+
sendRegistrationCode,
|
|
93
|
+
"sendRegistrationCode",
|
|
94
|
+
config.codeDelivery
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const verificationCode = generateNumericVerificationCode(
|
|
98
|
+
config.registration.codeDigits
|
|
99
|
+
);
|
|
100
|
+
const codeGeneratedAt = new Date();
|
|
101
|
+
const codeExpiresAt = new Date(
|
|
102
|
+
codeGeneratedAt.getTime() + config.registration.codeTtlMs
|
|
103
|
+
);
|
|
104
|
+
const verificationCodeHash = await hashVerificationCode(verificationCode);
|
|
105
|
+
const potentialUserCreated = await repository.upsertPotentialUser(
|
|
106
|
+
normalizedEmail,
|
|
107
|
+
verificationCodeHash,
|
|
108
|
+
codeGeneratedAt,
|
|
109
|
+
codeExpiresAt
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
if (!potentialUserCreated) {
|
|
113
|
+
return res.status(409).json({ message: "User already exists" });
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (sendRegistrationCode) {
|
|
117
|
+
await sendRegistrationCode({
|
|
118
|
+
email: normalizedEmail,
|
|
119
|
+
verificationCode,
|
|
120
|
+
codeGeneratedAt,
|
|
121
|
+
codeExpiresAt
|
|
122
|
+
});
|
|
123
|
+
return res.status(202).json({ message: "Verification code sent" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return res.status(201).json({
|
|
127
|
+
email: normalizedEmail,
|
|
128
|
+
verificationCode,
|
|
129
|
+
expiresAt: codeExpiresAt
|
|
130
|
+
});
|
|
131
|
+
} catch (error) {
|
|
132
|
+
next(error);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function registerUser(req, res, next) {
|
|
137
|
+
try {
|
|
138
|
+
const email = req.body?.email;
|
|
139
|
+
const password = req.body?.password;
|
|
140
|
+
|
|
141
|
+
if (!isNonEmptyString(email) || !isNonEmptyString(password)) {
|
|
142
|
+
return res.status(400).json({ message: "email and password are required" });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const normalizedEmail = normalizeEmail(email);
|
|
146
|
+
if (await repository.findUserByEmail(normalizedEmail)) {
|
|
147
|
+
return res.status(409).json({ message: "User already exists" });
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (config.registration.verificationRequired) {
|
|
151
|
+
const verificationCode = req.body?.verificationCode;
|
|
152
|
+
|
|
153
|
+
if (!isNonEmptyString(verificationCode)) {
|
|
154
|
+
return res.status(400).json({ message: "verificationCode is required" });
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const potentialUser = await repository.findPotentialUserByEmail(
|
|
158
|
+
normalizedEmail
|
|
159
|
+
);
|
|
160
|
+
const codeMatches = await codeMatchesRecord(
|
|
161
|
+
verificationCode,
|
|
162
|
+
potentialUser?.verificationCodeHash,
|
|
163
|
+
potentialUser?.codeExpiresAt
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
if (!codeMatches) {
|
|
167
|
+
return res.status(400).json({
|
|
168
|
+
message: "Invalid or expired verification code"
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
try {
|
|
174
|
+
const userId = await repository.insertUser(
|
|
175
|
+
normalizedEmail,
|
|
176
|
+
await hashPassword(password, config.passwordHash)
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
if (config.registration.verificationRequired) {
|
|
180
|
+
await repository.deletePotentialUserByEmail(normalizedEmail);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return res.status(201).json({ user_id: userId, email: normalizedEmail });
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (error.code === 11000) {
|
|
186
|
+
return res.status(409).json({ message: "User already exists" });
|
|
187
|
+
}
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
next(error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function generateTwoFactorCode(req, res, next) {
|
|
196
|
+
try {
|
|
197
|
+
if (!config.twoFactor.required) {
|
|
198
|
+
return res.status(400).json({ message: "Two-factor authentication is disabled" });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const user = await authenticatePrimaryCredentials(req.body);
|
|
202
|
+
if (!user) {
|
|
203
|
+
return res.status(401).json({ message: "Invalid email or password" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
requireDeliveryCallback(
|
|
207
|
+
sendTwoFactorCode,
|
|
208
|
+
"sendTwoFactorCode",
|
|
209
|
+
config.codeDelivery
|
|
210
|
+
);
|
|
211
|
+
|
|
212
|
+
const verificationCode = generateNumericVerificationCode(
|
|
213
|
+
config.twoFactor.codeDigits
|
|
214
|
+
);
|
|
215
|
+
const codeGeneratedAt = new Date();
|
|
216
|
+
const codeExpiresAt = new Date(
|
|
217
|
+
codeGeneratedAt.getTime() + config.twoFactor.codeTtlMs
|
|
218
|
+
);
|
|
219
|
+
await repository.updateUserTwoFactorCode(
|
|
220
|
+
user.user_id,
|
|
221
|
+
await hashVerificationCode(verificationCode),
|
|
222
|
+
codeGeneratedAt,
|
|
223
|
+
codeExpiresAt
|
|
224
|
+
);
|
|
225
|
+
|
|
226
|
+
if (sendTwoFactorCode) {
|
|
227
|
+
await sendTwoFactorCode({
|
|
228
|
+
email: user.email,
|
|
229
|
+
verificationCode,
|
|
230
|
+
codeGeneratedAt,
|
|
231
|
+
codeExpiresAt
|
|
232
|
+
});
|
|
233
|
+
return res.status(202).json({ message: "Two-factor code sent" });
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return res.status(201).json({
|
|
237
|
+
email: user.email,
|
|
238
|
+
verificationCode,
|
|
239
|
+
expiresAt: codeExpiresAt
|
|
240
|
+
});
|
|
241
|
+
} catch (error) {
|
|
242
|
+
next(error);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async function issueToken(req, res, next) {
|
|
247
|
+
try {
|
|
248
|
+
const user = await authenticatePrimaryCredentials(req.body);
|
|
249
|
+
if (!user) {
|
|
250
|
+
return res.status(401).json({ message: "Invalid email or password" });
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (config.twoFactor.required) {
|
|
254
|
+
const twoFactorCode = req.body?.twoFactorCode;
|
|
255
|
+
const codeMatches = await codeMatchesRecord(
|
|
256
|
+
twoFactorCode,
|
|
257
|
+
user.twoFactorCodeHash,
|
|
258
|
+
user.twoFactorCodeExpiresAt
|
|
259
|
+
);
|
|
260
|
+
|
|
261
|
+
if (!codeMatches) {
|
|
262
|
+
return res.status(401).json({
|
|
263
|
+
message: twoFactorCode
|
|
264
|
+
? "Invalid or expired two-factor code"
|
|
265
|
+
: "Two-factor code is required"
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const payload = await payloadFactory(user);
|
|
271
|
+
const tokens = await tokenService.createTokenPair(payload, user.user_id);
|
|
272
|
+
|
|
273
|
+
if (config.twoFactor.required) {
|
|
274
|
+
const codeConsumed = await repository.consumeUserTwoFactorCode(
|
|
275
|
+
user.user_id,
|
|
276
|
+
user.twoFactorCodeHash
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
if (!codeConsumed) {
|
|
280
|
+
return res.status(401).json({
|
|
281
|
+
message: "Invalid or already used two-factor code"
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
setRefreshCookie(res, tokens.refreshToken, config.refreshCookie);
|
|
287
|
+
return res.json({ accessToken: tokens.accessToken });
|
|
288
|
+
} catch (error) {
|
|
289
|
+
next(error);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
async function refreshToken(req, res, next) {
|
|
294
|
+
try {
|
|
295
|
+
const refreshTokenFromBody = req.body?.refresh_token;
|
|
296
|
+
const cookies = parseCookie(req.headers.cookie || "");
|
|
297
|
+
const refreshToken =
|
|
298
|
+
isNonEmptyString(refreshTokenFromBody)
|
|
299
|
+
? refreshTokenFromBody
|
|
300
|
+
: cookies[config.refreshCookie.name];
|
|
301
|
+
|
|
302
|
+
if (!refreshToken) {
|
|
303
|
+
return res.status(401).json({ message: "Refresh token is required" });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
let tokenPayload;
|
|
307
|
+
try {
|
|
308
|
+
tokenPayload = await tokenService.verifyRefreshToken(refreshToken);
|
|
309
|
+
} catch {
|
|
310
|
+
return res.status(401).json({ message: "Invalid or expired refresh token" });
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const user = await repository.findUserByUserId(tokenPayload.sub);
|
|
314
|
+
if (!user) {
|
|
315
|
+
return res.status(401).json({ message: "Invalid or expired refresh token" });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const payload = await payloadFactory(user);
|
|
319
|
+
const tokens = await tokenService.createTokenPair(payload, user.user_id);
|
|
320
|
+
setRefreshCookie(res, tokens.refreshToken, config.refreshCookie);
|
|
321
|
+
return res.json({ accessToken: tokens.accessToken });
|
|
322
|
+
} catch (error) {
|
|
323
|
+
next(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async function changePassword(req, res, next) {
|
|
328
|
+
try {
|
|
329
|
+
const newPassword = req.body?.newPassword;
|
|
330
|
+
const requestedUserId = req.body?.user_id;
|
|
331
|
+
const requestedEmail = req.body?.email;
|
|
332
|
+
|
|
333
|
+
if (
|
|
334
|
+
!isNonEmptyString(newPassword) ||
|
|
335
|
+
(requestedUserId !== undefined && !isNonEmptyString(requestedUserId)) ||
|
|
336
|
+
(requestedEmail !== undefined && !isNonEmptyString(requestedEmail)) ||
|
|
337
|
+
(requestedUserId !== undefined && requestedEmail !== undefined)
|
|
338
|
+
) {
|
|
339
|
+
return res.status(400).json({
|
|
340
|
+
message: "newPassword is required; provide either user_id or email, not both"
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const authenticatedUserId = req.auth?.sub;
|
|
345
|
+
if (!isNonEmptyString(authenticatedUserId)) {
|
|
346
|
+
return res.status(401).json({ message: "Invalid access token" });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
const targetUser = requestedUserId
|
|
350
|
+
? await repository.findUserByUserId(requestedUserId)
|
|
351
|
+
: requestedEmail
|
|
352
|
+
? await repository.findUserByEmail(normalizeEmail(requestedEmail))
|
|
353
|
+
: await repository.findUserByUserId(authenticatedUserId);
|
|
354
|
+
|
|
355
|
+
if (!targetUser) {
|
|
356
|
+
return res.status(404).json({ message: "User not found" });
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (
|
|
360
|
+
targetUser.user_id !== authenticatedUserId &&
|
|
361
|
+
(!req.auth?.nab_role?.includes(config.authorization.adminRole) ||
|
|
362
|
+
!config.authorization.nabAdminCanChangeOtherUsersPasswords)
|
|
363
|
+
) {
|
|
364
|
+
return res.status(403).json({
|
|
365
|
+
message: "Nab admin permission is required to change another user's password"
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const userUpdated = await repository.updateUserPassword(
|
|
370
|
+
targetUser.user_id,
|
|
371
|
+
await hashPassword(newPassword, config.passwordHash)
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
return userUpdated
|
|
375
|
+
? res.status(204).send()
|
|
376
|
+
: res.status(404).json({ message: "User not found" });
|
|
377
|
+
} catch (error) {
|
|
378
|
+
next(error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
async function addRoles(req, res, next) {
|
|
383
|
+
try {
|
|
384
|
+
const normalized = normalizeRoles(req.body?.roles);
|
|
385
|
+
if (normalized.error) return res.status(400).json({ message: normalized.error });
|
|
386
|
+
|
|
387
|
+
const target = await findTargetUser(req.body);
|
|
388
|
+
if (target.error) return res.status(400).json({ message: target.error });
|
|
389
|
+
if (!target.user) return res.status(404).json({ message: "User not found" });
|
|
390
|
+
|
|
391
|
+
return res.json(
|
|
392
|
+
roleResponse(await repository.addUserRoles(target.user.user_id, normalized.roles))
|
|
393
|
+
);
|
|
394
|
+
} catch (error) {
|
|
395
|
+
next(error);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
async function removeRole(req, res, next) {
|
|
400
|
+
try {
|
|
401
|
+
const role = req.body?.role;
|
|
402
|
+
if (!isNonEmptyString(role)) {
|
|
403
|
+
return res.status(400).json({ message: "role must be a non-empty string" });
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const target = await findTargetUser(req.body);
|
|
407
|
+
if (target.error) return res.status(400).json({ message: target.error });
|
|
408
|
+
if (!target.user) return res.status(404).json({ message: "User not found" });
|
|
409
|
+
|
|
410
|
+
return res.json(
|
|
411
|
+
roleResponse(await repository.removeUserRole(target.user.user_id, role.trim()))
|
|
412
|
+
);
|
|
413
|
+
} catch (error) {
|
|
414
|
+
next(error);
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
async function replaceRoles(req, res, next) {
|
|
419
|
+
try {
|
|
420
|
+
const normalized = normalizeRoles(req.body?.roles, true);
|
|
421
|
+
if (normalized.error) return res.status(400).json({ message: normalized.error });
|
|
422
|
+
|
|
423
|
+
const target = await findTargetUser(req.body);
|
|
424
|
+
if (target.error) return res.status(400).json({ message: target.error });
|
|
425
|
+
if (!target.user) return res.status(404).json({ message: "User not found" });
|
|
426
|
+
|
|
427
|
+
return res.json(
|
|
428
|
+
roleResponse(
|
|
429
|
+
await repository.replaceUserRoles(target.user.user_id, normalized.roles)
|
|
430
|
+
)
|
|
431
|
+
);
|
|
432
|
+
} catch (error) {
|
|
433
|
+
next(error);
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
async function authenticatePrimaryCredentials(body) {
|
|
438
|
+
const email = body?.email;
|
|
439
|
+
const password = body?.password;
|
|
440
|
+
|
|
441
|
+
if (!isNonEmptyString(email) || !isNonEmptyString(password)) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const user = await repository.findUserByEmail(normalizeEmail(email));
|
|
446
|
+
const passwordMatches = await argon2.verify(
|
|
447
|
+
user?.passwordHash || dummyPasswordHash,
|
|
448
|
+
password
|
|
449
|
+
);
|
|
450
|
+
return user && passwordMatches ? user : null;
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return {
|
|
454
|
+
createPotentialUser,
|
|
455
|
+
registerUser,
|
|
456
|
+
generateTwoFactorCode,
|
|
457
|
+
issueToken,
|
|
458
|
+
refreshToken,
|
|
459
|
+
changePassword,
|
|
460
|
+
addRoles,
|
|
461
|
+
removeRole,
|
|
462
|
+
replaceRoles
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async function codeMatchesRecord(code, codeHash, expiresAt) {
|
|
467
|
+
if (
|
|
468
|
+
!isNonEmptyString(code) ||
|
|
469
|
+
!isNonEmptyString(codeHash) ||
|
|
470
|
+
!(expiresAt instanceof Date) ||
|
|
471
|
+
expiresAt.getTime() <= Date.now()
|
|
472
|
+
) {
|
|
473
|
+
return false;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
try {
|
|
477
|
+
return await verifyVerificationCode(code, codeHash);
|
|
478
|
+
} catch {
|
|
479
|
+
return false;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function setRefreshCookie(res, refreshToken, cookieConfig) {
|
|
484
|
+
res.cookie(cookieConfig.name, refreshToken, {
|
|
485
|
+
httpOnly: cookieConfig.httpOnly,
|
|
486
|
+
secure: cookieConfig.secure,
|
|
487
|
+
sameSite: cookieConfig.sameSite,
|
|
488
|
+
path: cookieConfig.path,
|
|
489
|
+
maxAge: cookieConfig.maxAgeMs
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
async function hashPassword(password, passwordHashConfig) {
|
|
494
|
+
return argon2.hash(password, {
|
|
495
|
+
type: argon2.argon2id,
|
|
496
|
+
...passwordHashConfig
|
|
497
|
+
});
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function validateCallback(callback, name) {
|
|
501
|
+
if (callback !== undefined && typeof callback !== "function") {
|
|
502
|
+
throw new TypeError(`${name} must be a function`);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function requireDeliveryCallback(callback, name, codeDeliveryConfig) {
|
|
507
|
+
if (!callback && !codeDeliveryConfig.allowCodeInResponseWithoutSender) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
`${name} must be configured when code responses are disabled`
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function isNonEmptyString(value) {
|
|
515
|
+
return typeof value === "string" && !!value.trim();
|
|
516
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createAuthConfig } from "./config/createAuthConfig.js";
|
|
2
|
+
import { createAuthController } from "./controllers/authController.js";
|
|
3
|
+
import { createJwtMiddleware } from "./middleware/jwtMiddleware.js";
|
|
4
|
+
import { createUserRepository } from "./repositories/userRepository.js";
|
|
5
|
+
import { createAuthRouter } from "./routes/authRoutes.js";
|
|
6
|
+
import { createJwtKeyProvider } from "./services/jwtKeyProvider.js";
|
|
7
|
+
import { createTokenService } from "./services/tokenService.js";
|
|
8
|
+
|
|
9
|
+
export function createAuthenticationLibrary(configOverrides = {}) {
|
|
10
|
+
const config = createAuthConfig(configOverrides);
|
|
11
|
+
const keyProvider = createJwtKeyProvider(config.jwt);
|
|
12
|
+
const repository = createUserRepository({
|
|
13
|
+
mongo: config.mongo,
|
|
14
|
+
defaultAdmin: config.defaultAdmin,
|
|
15
|
+
passwordHash: config.passwordHash
|
|
16
|
+
});
|
|
17
|
+
const tokenService = createTokenService({ jwt: config.jwt, keyProvider });
|
|
18
|
+
const middleware = createJwtMiddleware({
|
|
19
|
+
jwt: config.jwt,
|
|
20
|
+
keyProvider,
|
|
21
|
+
authorization: config.authorization
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
config,
|
|
26
|
+
connectToDatabase: repository.connectToDatabase,
|
|
27
|
+
closeDatabase: repository.closeDatabase,
|
|
28
|
+
requireJwt: middleware.requireJwt,
|
|
29
|
+
requireNabAdmin: middleware.requireNabAdmin,
|
|
30
|
+
jwtErrorHandler: middleware.jwtErrorHandler,
|
|
31
|
+
createAuthRouter(payloadFactory, options = {}) {
|
|
32
|
+
const controller = createAuthController({
|
|
33
|
+
config,
|
|
34
|
+
repository,
|
|
35
|
+
tokenService,
|
|
36
|
+
payloadFactory,
|
|
37
|
+
options
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return createAuthRouter({ controller, middleware, config });
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { expressjwt } from "express-jwt";
|
|
2
|
+
|
|
3
|
+
export function createJwtMiddleware({ jwt: jwtConfig, keyProvider, authorization }) {
|
|
4
|
+
const requireJwt = expressjwt({
|
|
5
|
+
secret: async (req, token) => {
|
|
6
|
+
if (token?.header?.alg !== jwtConfig.algorithm) {
|
|
7
|
+
throw new Error("Invalid JWT algorithm");
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
return keyProvider.getPublicKey(token.header.kid);
|
|
11
|
+
},
|
|
12
|
+
algorithms: [jwtConfig.algorithm],
|
|
13
|
+
issuer: jwtConfig.issuer,
|
|
14
|
+
audience: jwtConfig.audience,
|
|
15
|
+
isRevoked: async (req, token) => token?.payload?.tokenType !== "access"
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
function requireNabAdmin(req, res, next) {
|
|
19
|
+
if (!req.auth?.nab_role?.includes(authorization.adminRole)) {
|
|
20
|
+
return res.status(403).json({ message: "Nab admin role is required" });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
next();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function jwtErrorHandler(error, req, res, next) {
|
|
27
|
+
if (error.name === "UnauthorizedError") {
|
|
28
|
+
return res.status(401).json({ message: "Invalid or missing JWT" });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
next(error);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return { requireJwt, requireNabAdmin, jwtErrorHandler };
|
|
35
|
+
}
|