@nabhiraj/nab-auth 0.0.0-stage → 0.1.0
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 +403 -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,281 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import argon2 from "argon2";
|
|
3
|
+
import { MongoClient } from "mongodb";
|
|
4
|
+
|
|
5
|
+
export function createUserRepository({ mongo, defaultAdmin, passwordHash }) {
|
|
6
|
+
let client;
|
|
7
|
+
let usersCollection;
|
|
8
|
+
let potentialUsersCollection;
|
|
9
|
+
|
|
10
|
+
function requireUsersCollection() {
|
|
11
|
+
if (!usersCollection) {
|
|
12
|
+
throw new Error("Database connection has not been initialized");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function requirePotentialUsersCollection() {
|
|
17
|
+
if (!potentialUsersCollection) {
|
|
18
|
+
throw new Error("Database connection has not been initialized");
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function findUserByEmail(email) {
|
|
23
|
+
requireUsersCollection();
|
|
24
|
+
return usersCollection.findOne({ email: normalizeEmail(email) });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function findUserByUserId(userId) {
|
|
28
|
+
requireUsersCollection();
|
|
29
|
+
return usersCollection.findOne({ user_id: userId });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function insertUser(email, passwordHashValue) {
|
|
33
|
+
requireUsersCollection();
|
|
34
|
+
const userId = randomUUID();
|
|
35
|
+
|
|
36
|
+
await usersCollection.insertOne({
|
|
37
|
+
user_id: userId,
|
|
38
|
+
email: normalizeEmail(email),
|
|
39
|
+
passwordHash: passwordHashValue,
|
|
40
|
+
nab_role: [],
|
|
41
|
+
createdAt: new Date()
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return userId;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function ensureDefaultAdminUser() {
|
|
48
|
+
if (!defaultAdmin.enabled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const existingAdmin = await findUserByEmail(defaultAdmin.email);
|
|
53
|
+
|
|
54
|
+
if (existingAdmin) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const passwordHashValue = await argon2.hash(defaultAdmin.password, {
|
|
59
|
+
type: argon2.argon2id,
|
|
60
|
+
...passwordHash
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
const userId = randomUUID();
|
|
65
|
+
await usersCollection.insertOne({
|
|
66
|
+
user_id: userId,
|
|
67
|
+
email: normalizeEmail(defaultAdmin.email),
|
|
68
|
+
passwordHash: passwordHashValue,
|
|
69
|
+
nab_role: defaultAdmin.roles,
|
|
70
|
+
createdAt: new Date()
|
|
71
|
+
});
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (error.code !== 11000) {
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
async connectToDatabase() {
|
|
81
|
+
if (usersCollection) {
|
|
82
|
+
return usersCollection;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (typeof mongo.uri !== "string" || !mongo.uri) {
|
|
86
|
+
throw new Error("mongo.uri must be configured before connecting");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
client = new MongoClient(mongo.uri);
|
|
90
|
+
await client.connect();
|
|
91
|
+
|
|
92
|
+
const database = client.db(mongo.databaseName);
|
|
93
|
+
await ensureCollection(database, mongo.usersCollectionName);
|
|
94
|
+
await ensureCollection(database, mongo.potentialUsersCollectionName);
|
|
95
|
+
|
|
96
|
+
usersCollection = database.collection(mongo.usersCollectionName);
|
|
97
|
+
potentialUsersCollection = database.collection(
|
|
98
|
+
mongo.potentialUsersCollectionName
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
await addMissingUserIds(usersCollection);
|
|
102
|
+
await addMissingUserRoles(usersCollection);
|
|
103
|
+
await usersCollection.createIndex({ email: 1 }, { unique: true });
|
|
104
|
+
await usersCollection.createIndex({ user_id: 1 }, { unique: true });
|
|
105
|
+
await potentialUsersCollection.createIndex({ email: 1 }, { unique: true });
|
|
106
|
+
await potentialUsersCollection.createIndex(
|
|
107
|
+
{ expiresAt: 1 },
|
|
108
|
+
{ expireAfterSeconds: 0 }
|
|
109
|
+
);
|
|
110
|
+
await ensureDefaultAdminUser();
|
|
111
|
+
|
|
112
|
+
return usersCollection;
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
async closeDatabase() {
|
|
116
|
+
usersCollection = undefined;
|
|
117
|
+
potentialUsersCollection = undefined;
|
|
118
|
+
|
|
119
|
+
if (client) {
|
|
120
|
+
await client.close();
|
|
121
|
+
client = undefined;
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
|
|
125
|
+
findUserByEmail,
|
|
126
|
+
findUserByUserId,
|
|
127
|
+
insertUser,
|
|
128
|
+
|
|
129
|
+
async upsertPotentialUser(
|
|
130
|
+
email,
|
|
131
|
+
verificationCodeHash,
|
|
132
|
+
codeGeneratedAt,
|
|
133
|
+
codeExpiresAt
|
|
134
|
+
) {
|
|
135
|
+
requirePotentialUsersCollection();
|
|
136
|
+
|
|
137
|
+
if (await findUserByEmail(email)) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
await potentialUsersCollection.updateOne(
|
|
142
|
+
{ email: normalizeEmail(email) },
|
|
143
|
+
{
|
|
144
|
+
$set: {
|
|
145
|
+
verificationCodeHash,
|
|
146
|
+
codeGeneratedAt,
|
|
147
|
+
codeExpiresAt,
|
|
148
|
+
expiresAt: codeExpiresAt,
|
|
149
|
+
updatedAt: new Date()
|
|
150
|
+
},
|
|
151
|
+
$setOnInsert: { createdAt: new Date() }
|
|
152
|
+
},
|
|
153
|
+
{ upsert: true }
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
return true;
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
async findPotentialUserByEmail(email) {
|
|
160
|
+
requirePotentialUsersCollection();
|
|
161
|
+
return potentialUsersCollection.findOne({ email: normalizeEmail(email) });
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
async deletePotentialUserByEmail(email) {
|
|
165
|
+
requirePotentialUsersCollection();
|
|
166
|
+
await potentialUsersCollection.deleteOne({ email: normalizeEmail(email) });
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
async updateUserPassword(userId, passwordHashValue) {
|
|
170
|
+
requireUsersCollection();
|
|
171
|
+
const result = await usersCollection.updateOne(
|
|
172
|
+
{ user_id: userId },
|
|
173
|
+
{ $set: { passwordHash: passwordHashValue, updatedAt: new Date() } }
|
|
174
|
+
);
|
|
175
|
+
return result.matchedCount > 0;
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
async updateUserTwoFactorCode(
|
|
179
|
+
userId,
|
|
180
|
+
twoFactorCodeHash,
|
|
181
|
+
twoFactorCodeGeneratedAt,
|
|
182
|
+
twoFactorCodeExpiresAt
|
|
183
|
+
) {
|
|
184
|
+
requireUsersCollection();
|
|
185
|
+
const result = await usersCollection.updateOne(
|
|
186
|
+
{ user_id: userId },
|
|
187
|
+
{
|
|
188
|
+
$set: {
|
|
189
|
+
twoFactorCodeHash,
|
|
190
|
+
twoFactorCodeGeneratedAt,
|
|
191
|
+
twoFactorCodeExpiresAt,
|
|
192
|
+
updatedAt: new Date()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
);
|
|
196
|
+
return result.matchedCount > 0;
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
async consumeUserTwoFactorCode(userId, twoFactorCodeHash) {
|
|
200
|
+
requireUsersCollection();
|
|
201
|
+
const result = await usersCollection.updateOne(
|
|
202
|
+
{ user_id: userId, twoFactorCodeHash },
|
|
203
|
+
{
|
|
204
|
+
$unset: {
|
|
205
|
+
twoFactorCodeHash: "",
|
|
206
|
+
twoFactorCodeGeneratedAt: "",
|
|
207
|
+
twoFactorCodeExpiresAt: ""
|
|
208
|
+
},
|
|
209
|
+
$set: { updatedAt: new Date() }
|
|
210
|
+
}
|
|
211
|
+
);
|
|
212
|
+
return result.modifiedCount > 0;
|
|
213
|
+
},
|
|
214
|
+
|
|
215
|
+
async addUserRoles(userId, roles) {
|
|
216
|
+
return updateUserRoles(usersCollection, userId, {
|
|
217
|
+
$addToSet: { nab_role: { $each: roles } }
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
async removeUserRole(userId, role) {
|
|
222
|
+
return updateUserRoles(usersCollection, userId, { $pull: { nab_role: role } });
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
async replaceUserRoles(userId, roles) {
|
|
226
|
+
return updateUserRoles(usersCollection, userId, { $set: { nab_role: roles } });
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function ensureCollection(database, collectionName) {
|
|
232
|
+
const collectionExists = await database
|
|
233
|
+
.listCollections({ name: collectionName }, { nameOnly: true })
|
|
234
|
+
.hasNext();
|
|
235
|
+
|
|
236
|
+
if (collectionExists) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
await database.createCollection(collectionName);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
if (error.code !== 48) {
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function addMissingUserIds(usersCollection) {
|
|
250
|
+
const filter = {
|
|
251
|
+
$or: [{ user_id: { $exists: false } }, { user_id: null }, { user_id: "" }]
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
for await (const user of usersCollection.find(filter)) {
|
|
255
|
+
await usersCollection.updateOne(
|
|
256
|
+
{ _id: user._id, ...filter },
|
|
257
|
+
{ $set: { user_id: randomUUID() } }
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function addMissingUserRoles(usersCollection) {
|
|
263
|
+
await usersCollection.updateMany(
|
|
264
|
+
{ nab_role: { $exists: false } },
|
|
265
|
+
{ $set: { nab_role: [] } }
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
async function updateUserRoles(usersCollection, userId, update) {
|
|
270
|
+
const result = await usersCollection.updateOne({ user_id: userId }, update);
|
|
271
|
+
|
|
272
|
+
if (result.matchedCount === 0) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return usersCollection.findOne({ user_id: userId });
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
export function normalizeEmail(email) {
|
|
280
|
+
return email.trim().toLowerCase();
|
|
281
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Router } from "express";
|
|
2
|
+
|
|
3
|
+
export function createAuthRouter({ controller, middleware, config }) {
|
|
4
|
+
const authRouter = Router();
|
|
5
|
+
|
|
6
|
+
const registrationMiddleware = config.authorization.userRegistrationRequiresNabAdmin
|
|
7
|
+
? [middleware.requireJwt, middleware.requireNabAdmin]
|
|
8
|
+
: [];
|
|
9
|
+
const passwordChangeMiddleware = config.authorization.passwordChangeRequiresNabAdmin
|
|
10
|
+
? [middleware.requireJwt, middleware.requireNabAdmin]
|
|
11
|
+
: [middleware.requireJwt];
|
|
12
|
+
const roleManagementMiddleware = [
|
|
13
|
+
middleware.requireJwt,
|
|
14
|
+
middleware.requireNabAdmin
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
authRouter.post("/roles", ...roleManagementMiddleware, controller.addRoles);
|
|
18
|
+
authRouter.delete(
|
|
19
|
+
"/roles",
|
|
20
|
+
...roleManagementMiddleware,
|
|
21
|
+
controller.removeRole
|
|
22
|
+
);
|
|
23
|
+
authRouter.put(
|
|
24
|
+
"/roles",
|
|
25
|
+
...roleManagementMiddleware,
|
|
26
|
+
controller.replaceRoles
|
|
27
|
+
);
|
|
28
|
+
authRouter.patch(
|
|
29
|
+
"/password",
|
|
30
|
+
...passwordChangeMiddleware,
|
|
31
|
+
controller.changePassword
|
|
32
|
+
);
|
|
33
|
+
authRouter.post("/potential-users", controller.createPotentialUser);
|
|
34
|
+
authRouter.post("/2fa/code", controller.generateTwoFactorCode);
|
|
35
|
+
authRouter.post("/register", ...registrationMiddleware, controller.registerUser);
|
|
36
|
+
authRouter.post("/token", controller.issueToken);
|
|
37
|
+
authRouter.post("/refresh", controller.refreshToken);
|
|
38
|
+
|
|
39
|
+
return authRouter;
|
|
40
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
|
|
4
|
+
export function createJwtKeyProvider(jwtConfig) {
|
|
5
|
+
return jwtConfig.keyProvider || createFileJwtKeyProvider(jwtConfig);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function createFileJwtKeyProvider({ keys, keyCacheTtlMs }) {
|
|
9
|
+
let publicKeyCache;
|
|
10
|
+
let privateKeyCache;
|
|
11
|
+
|
|
12
|
+
async function loadPublicKeyCache() {
|
|
13
|
+
const publicKeyConfig = await readJsonFile(
|
|
14
|
+
keys.publicKeyPath,
|
|
15
|
+
"jwt.keys.publicKeyPath"
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
if (!Array.isArray(publicKeyConfig.keys)) {
|
|
19
|
+
throw new Error("Public JWT key file must contain a keys array");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const publicKeys = new Map();
|
|
23
|
+
|
|
24
|
+
for (const key of publicKeyConfig.keys) {
|
|
25
|
+
if (typeof key?.kid !== "string" || typeof key.publicKey !== "string") {
|
|
26
|
+
throw new Error(
|
|
27
|
+
"Each public JWT key must contain string fields: kid, publicKey"
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (publicKeys.has(key.kid)) {
|
|
32
|
+
throw new Error(`Duplicate public JWT key id: ${key.kid}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
publicKeys.set(key.kid, key.publicKey);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
publicKeyCache = {
|
|
39
|
+
expiresAt: Date.now() + keyCacheTtlMs,
|
|
40
|
+
publicKeys
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function ensurePublicKeyCache() {
|
|
45
|
+
if (!publicKeyCache || Date.now() >= publicKeyCache.expiresAt) {
|
|
46
|
+
await loadPublicKeyCache();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getPublicKey(keyId) {
|
|
51
|
+
await ensurePublicKeyCache();
|
|
52
|
+
|
|
53
|
+
const publicKey = publicKeyCache.publicKeys.get(keyId);
|
|
54
|
+
|
|
55
|
+
if (!publicKey) {
|
|
56
|
+
throw new Error(`Unknown JWT key id: ${keyId || "missing"}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return publicKey;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async function getActiveSigningKey() {
|
|
63
|
+
if (!privateKeyCache || Date.now() >= privateKeyCache.expiresAt) {
|
|
64
|
+
const privateKeyConfig = await readJsonFile(
|
|
65
|
+
keys.privateKeyPath,
|
|
66
|
+
"jwt.keys.privateKeyPath"
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (
|
|
70
|
+
typeof privateKeyConfig.kid !== "string" ||
|
|
71
|
+
typeof privateKeyConfig.privateKey !== "string"
|
|
72
|
+
) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Private JWT key file must contain string fields: kid, privateKey"
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
await getPublicKey(privateKeyConfig.kid);
|
|
79
|
+
|
|
80
|
+
privateKeyCache = {
|
|
81
|
+
expiresAt: Date.now() + keyCacheTtlMs,
|
|
82
|
+
keyId: privateKeyConfig.kid,
|
|
83
|
+
privateKey: privateKeyConfig.privateKey
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
keyId: privateKeyCache.keyId,
|
|
89
|
+
privateKey: privateKeyCache.privateKey
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { getActiveSigningKey, getPublicKey };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function readJsonFile(pathValue, keyName) {
|
|
97
|
+
if (typeof pathValue !== "string" || !pathValue) {
|
|
98
|
+
throw new Error(`${keyName} must be set`);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const keyPath = resolve(process.cwd(), pathValue);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return JSON.parse(await readFile(keyPath, "utf8"));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`Unable to read ${keyName} at ${keyPath}: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
|
|
3
|
+
export function createTokenService({ jwt: jwtConfig, keyProvider }) {
|
|
4
|
+
function validatePayload(payload) {
|
|
5
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
6
|
+
throw new TypeError("Token payload must be an object");
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function signAccessToken(payload, subject, signingKey) {
|
|
11
|
+
return jwt.sign({ ...payload, tokenType: "access" }, signingKey.privateKey, {
|
|
12
|
+
subject,
|
|
13
|
+
issuer: jwtConfig.issuer,
|
|
14
|
+
audience: jwtConfig.audience,
|
|
15
|
+
expiresIn: jwtConfig.accessTokenTtl,
|
|
16
|
+
algorithm: jwtConfig.algorithm,
|
|
17
|
+
keyid: signingKey.keyId
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function signRefreshToken(subject, signingKey) {
|
|
22
|
+
return jwt.sign({ tokenType: "refresh" }, signingKey.privateKey, {
|
|
23
|
+
subject,
|
|
24
|
+
issuer: jwtConfig.issuer,
|
|
25
|
+
audience: jwtConfig.audience,
|
|
26
|
+
expiresIn: jwtConfig.refreshTokenTtl,
|
|
27
|
+
algorithm: jwtConfig.algorithm,
|
|
28
|
+
keyid: signingKey.keyId
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
async createAccessToken(payload, subject) {
|
|
34
|
+
validatePayload(payload);
|
|
35
|
+
return signAccessToken(payload, subject, await keyProvider.getActiveSigningKey());
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async createRefreshToken(subject) {
|
|
39
|
+
return signRefreshToken(subject, await keyProvider.getActiveSigningKey());
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
async createTokenPair(payload, subject) {
|
|
43
|
+
validatePayload(payload);
|
|
44
|
+
const signingKey = await keyProvider.getActiveSigningKey();
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
accessToken: signAccessToken(payload, subject, signingKey),
|
|
48
|
+
refreshToken: signRefreshToken(subject, signingKey)
|
|
49
|
+
};
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
async verifyRefreshToken(token) {
|
|
53
|
+
const decoded = jwt.decode(token, { complete: true });
|
|
54
|
+
|
|
55
|
+
if (
|
|
56
|
+
!decoded ||
|
|
57
|
+
typeof decoded !== "object" ||
|
|
58
|
+
typeof decoded.header?.kid !== "string" ||
|
|
59
|
+
decoded.header.alg !== jwtConfig.algorithm
|
|
60
|
+
) {
|
|
61
|
+
throw new Error("Invalid refresh token header");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const payload = jwt.verify(
|
|
65
|
+
token,
|
|
66
|
+
await keyProvider.getPublicKey(decoded.header.kid),
|
|
67
|
+
{
|
|
68
|
+
algorithms: [jwtConfig.algorithm],
|
|
69
|
+
issuer: jwtConfig.issuer,
|
|
70
|
+
audience: jwtConfig.audience
|
|
71
|
+
}
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
if (
|
|
75
|
+
typeof payload !== "object" ||
|
|
76
|
+
payload === null ||
|
|
77
|
+
payload.tokenType !== "refresh" ||
|
|
78
|
+
typeof payload.sub !== "string"
|
|
79
|
+
) {
|
|
80
|
+
throw new Error("Invalid refresh token");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return payload;
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { randomInt } from "node:crypto";
|
|
2
|
+
import argon2 from "argon2";
|
|
3
|
+
|
|
4
|
+
export function generateNumericVerificationCode(digits) {
|
|
5
|
+
if (!Number.isInteger(digits) || digits < 1 || digits > 9) {
|
|
6
|
+
throw new TypeError("Verification code digits must be an integer from 1 to 9");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
const minimum = digits === 1 ? 0 : 10 ** (digits - 1);
|
|
10
|
+
const maximum = 10 ** digits;
|
|
11
|
+
|
|
12
|
+
return String(randomInt(minimum, maximum)).padStart(digits, "0");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function hashVerificationCode(code) {
|
|
16
|
+
return argon2.hash(code, {
|
|
17
|
+
type: argon2.argon2id,
|
|
18
|
+
memoryCost: 19456,
|
|
19
|
+
timeCost: 2,
|
|
20
|
+
parallelism: 1
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function verifyVerificationCode(code, codeHash) {
|
|
25
|
+
return argon2.verify(codeHash, code);
|
|
26
|
+
}
|