@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nabhiraj
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,363 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nab-auth
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](LICENSE)
|
|
4
|
+
|
|
5
|
+
Configurable authentication for Express and MongoDB. `nab-auth` provides
|
|
6
|
+
Argon2id password hashing, RS256 JWTs, refresh-token rotation, roles,
|
|
7
|
+
registration verification, and optional two-factor authentication.
|
|
8
|
+
|
|
9
|
+
The library does not load environment variables or send email/SMS itself. The
|
|
10
|
+
host application owns configuration and code delivery.
|
|
11
|
+
|
|
12
|
+
## Features
|
|
13
|
+
|
|
14
|
+
- Email/password registration and login with Argon2id password hashes
|
|
15
|
+
- Short-lived RS256 access tokens and rotating refresh-token cookies
|
|
16
|
+
- Public-key rotation using JWT `kid` headers
|
|
17
|
+
- Role claims and administrator-protected role management
|
|
18
|
+
- Optional registration-code verification and two-factor authentication
|
|
19
|
+
- MongoDB/Atlas storage with unique and TTL indexes created automatically
|
|
20
|
+
- Verifier-only mode for services that only need to validate access tokens
|
|
21
|
+
|
|
22
|
+
## Requirements
|
|
23
|
+
|
|
24
|
+
- Node.js 20 or later
|
|
25
|
+
- Express 5
|
|
26
|
+
- MongoDB or MongoDB Atlas
|
|
27
|
+
- An RSA key pair for RS256 JWT signing
|
|
28
|
+
|
|
29
|
+
`argon2` is a native dependency. Most supported systems use a prebuilt binary;
|
|
30
|
+
unusual platforms may need standard Node.js native build tools.
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
npm install @nabhiraj/nab-auth express
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick start
|
|
39
|
+
|
|
40
|
+
```js
|
|
41
|
+
import express from "express";
|
|
42
|
+
import { createAuthenticationLibrary } from "@nabhiraj/nab-auth";
|
|
43
|
+
|
|
44
|
+
const app = express();
|
|
45
|
+
app.use(express.json());
|
|
46
|
+
|
|
47
|
+
const auth = createAuthenticationLibrary({
|
|
48
|
+
mongo: {
|
|
49
|
+
uri: process.env.MY_AUTH_MONGO_URI,
|
|
50
|
+
databaseName: "my_auth_database"
|
|
51
|
+
},
|
|
52
|
+
jwt: {
|
|
53
|
+
issuer: "my-service",
|
|
54
|
+
audience: "my-client",
|
|
55
|
+
keys: {
|
|
56
|
+
privateKeyPath: process.env.MY_JWT_PRIVATE_KEY_PATH,
|
|
57
|
+
publicKeyPath: process.env.MY_JWT_PUBLIC_KEY_PATH
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
defaultAdmin: { enabled: false }
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
const createTokenPayload = (user) => ({
|
|
64
|
+
email: user.email,
|
|
65
|
+
nab_role: user.nab_role || []
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
app.use(
|
|
69
|
+
"/auth",
|
|
70
|
+
auth.createAuthRouter(createTokenPayload, {
|
|
71
|
+
sendRegistrationCode: async ({ email, verificationCode }) => {
|
|
72
|
+
await sendEmail(email, `Your verification code is ${verificationCode}`);
|
|
73
|
+
},
|
|
74
|
+
sendTwoFactorCode: async ({ email, verificationCode }) => {
|
|
75
|
+
await sendEmail(email, `Your 2FA code is ${verificationCode}`);
|
|
76
|
+
}
|
|
77
|
+
})
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
app.get("/api/private", auth.requireJwt, (req, res) => {
|
|
81
|
+
res.json({ userId: req.auth.sub });
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
app.use(auth.jwtErrorHandler);
|
|
85
|
+
await auth.connectToDatabase();
|
|
86
|
+
app.listen(3000);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
`sendEmail` is an application function you provide. Configure a code-delivery
|
|
90
|
+
callback only when registration verification or two-factor authentication is
|
|
91
|
+
enabled.
|
|
92
|
+
|
|
93
|
+
## Core API
|
|
94
|
+
|
|
95
|
+
```js
|
|
96
|
+
import {
|
|
97
|
+
createAuthenticationLibrary,
|
|
98
|
+
createFileJwtKeyProvider,
|
|
99
|
+
defaultAuthConfig
|
|
100
|
+
} from "@nabhiraj/nab-auth";
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`createAuthenticationLibrary(config)` creates isolated state: its MongoDB
|
|
104
|
+
client, JWT-key cache, middleware, and router do not share state with another
|
|
105
|
+
instance.
|
|
106
|
+
|
|
107
|
+
| Member | Purpose |
|
|
108
|
+
| --- | --- |
|
|
109
|
+
| `connectToDatabase()` | Connects to MongoDB and creates configured collections/indexes. |
|
|
110
|
+
| `closeDatabase()` | Closes this instance's MongoDB client. |
|
|
111
|
+
| `createAuthRouter(payloadFactory, options)` | Creates the Express authentication router. |
|
|
112
|
+
| `requireJwt` | Verifies access tokens and sets claims on `req.auth`. |
|
|
113
|
+
| `requireNabAdmin` | Requires the configured administrator role. |
|
|
114
|
+
| `jwtErrorHandler` | Express JWT error middleware; mount after protected routes. |
|
|
115
|
+
| `config` | The merged configuration used by this instance. |
|
|
116
|
+
|
|
117
|
+
`payloadFactory(user)` creates application-specific access-token claims. The
|
|
118
|
+
library adds standard JWT claims, `sub`, and `tokenType: "access"`.
|
|
119
|
+
|
|
120
|
+
## Configuration
|
|
121
|
+
|
|
122
|
+
Every setting is optional unless a feature needs it. Omitted values use
|
|
123
|
+
`defaultAuthConfig`.
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
const auth = createAuthenticationLibrary({
|
|
127
|
+
mongo: {
|
|
128
|
+
uri: process.env.MY_AUTH_MONGO_URI,
|
|
129
|
+
databaseName: "auth_db",
|
|
130
|
+
usersCollectionName: "users",
|
|
131
|
+
potentialUsersCollectionName: "potential_user"
|
|
132
|
+
},
|
|
133
|
+
jwt: {
|
|
134
|
+
issuer: "my-service",
|
|
135
|
+
audience: "my-client",
|
|
136
|
+
algorithm: "RS256",
|
|
137
|
+
accessTokenTtl: "15m",
|
|
138
|
+
refreshTokenTtl: "7d",
|
|
139
|
+
keyCacheTtlMs: 15 * 60 * 1000,
|
|
140
|
+
keys: {
|
|
141
|
+
privateKeyPath: process.env.MY_PRIVATE_KEY_PATH,
|
|
142
|
+
publicKeyPath: process.env.MY_PUBLIC_KEY_PATH
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
authorization: {
|
|
146
|
+
adminRole: "nab_admin",
|
|
147
|
+
userRegistrationRequiresNabAdmin: false,
|
|
148
|
+
passwordChangeRequiresNabAdmin: false,
|
|
149
|
+
nabAdminCanChangeOtherUsersPasswords: true
|
|
150
|
+
},
|
|
151
|
+
registration: {
|
|
152
|
+
verificationRequired: false,
|
|
153
|
+
codeDigits: 6,
|
|
154
|
+
codeTtlMs: 10 * 60 * 1000
|
|
155
|
+
},
|
|
156
|
+
twoFactor: {
|
|
157
|
+
required: false,
|
|
158
|
+
codeDigits: 6,
|
|
159
|
+
codeTtlMs: 10 * 60 * 1000
|
|
160
|
+
},
|
|
161
|
+
passwordHash: {
|
|
162
|
+
memoryCost: 19456,
|
|
163
|
+
timeCost: 2,
|
|
164
|
+
parallelism: 1
|
|
165
|
+
},
|
|
166
|
+
refreshCookie: {
|
|
167
|
+
name: "refresh_token",
|
|
168
|
+
httpOnly: true,
|
|
169
|
+
secure: true,
|
|
170
|
+
sameSite: "lax",
|
|
171
|
+
path: "/auth/refresh",
|
|
172
|
+
maxAgeMs: 7 * 24 * 60 * 60 * 1000
|
|
173
|
+
},
|
|
174
|
+
codeDelivery: {
|
|
175
|
+
allowCodeInResponseWithoutSender: false
|
|
176
|
+
},
|
|
177
|
+
defaultAdmin: {
|
|
178
|
+
enabled: false,
|
|
179
|
+
email: "admin@admin.com",
|
|
180
|
+
password: "change-this",
|
|
181
|
+
roles: ["nab_admin"]
|
|
182
|
+
}
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Use `refreshCookie.secure: true` for HTTPS production deployments. It defaults
|
|
187
|
+
to `false` only for local HTTP development. `defaultAdmin.enabled` defaults to
|
|
188
|
+
`false`; never enable it with a known or weak password.
|
|
189
|
+
|
|
190
|
+
### Code delivery callbacks
|
|
191
|
+
|
|
192
|
+
```js
|
|
193
|
+
auth.createAuthRouter(createTokenPayload, {
|
|
194
|
+
sendRegistrationCode: async ({
|
|
195
|
+
email,
|
|
196
|
+
verificationCode,
|
|
197
|
+
codeGeneratedAt,
|
|
198
|
+
codeExpiresAt
|
|
199
|
+
}) => {
|
|
200
|
+
await emailProvider.send(email, verificationCode);
|
|
201
|
+
},
|
|
202
|
+
sendTwoFactorCode: async ({
|
|
203
|
+
email,
|
|
204
|
+
verificationCode,
|
|
205
|
+
codeGeneratedAt,
|
|
206
|
+
codeExpiresAt
|
|
207
|
+
}) => {
|
|
208
|
+
await smsProvider.send(email, verificationCode);
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`codeDelivery.allowCodeInResponseWithoutSender` controls missing callbacks:
|
|
214
|
+
|
|
215
|
+
- `true` returns the generated code in the HTTP response. Use only for local
|
|
216
|
+
development or automated tests.
|
|
217
|
+
- `false` rejects the request when the required sender callback is absent. Use
|
|
218
|
+
this in production.
|
|
219
|
+
|
|
220
|
+
Registration codes are stored as Argon2id hashes in `potential_user` and are
|
|
221
|
+
removed by a MongoDB TTL index. 2FA code hashes are stored on the user and are
|
|
222
|
+
atomically consumed after a successful token issue.
|
|
223
|
+
|
|
224
|
+
## JWT key files and rotation
|
|
225
|
+
|
|
226
|
+
The built-in key provider loads JSON files lazily and refreshes its cache after
|
|
227
|
+
`jwt.keyCacheTtlMs`.
|
|
228
|
+
|
|
229
|
+
Private-key JSON has one active signing key:
|
|
230
|
+
|
|
231
|
+
```json
|
|
232
|
+
{
|
|
233
|
+
"kid": "101",
|
|
234
|
+
"privateKey": "-----BEGIN PRIVATE KEY-----..."
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Public-key JSON keeps each currently valid verification key:
|
|
239
|
+
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"keys": [
|
|
243
|
+
{
|
|
244
|
+
"kid": "101",
|
|
245
|
+
"publicKey": "-----BEGIN PUBLIC KEY-----..."
|
|
246
|
+
}
|
|
247
|
+
]
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Rotation process:
|
|
252
|
+
|
|
253
|
+
1. Add the new public key and `kid` to the public-key JSON.
|
|
254
|
+
2. Switch the active private-key JSON to the matching new key and `kid`.
|
|
255
|
+
3. Keep the previous public key until all tokens signed by it have expired.
|
|
256
|
+
|
|
257
|
+
Never commit or publish private keys.
|
|
258
|
+
|
|
259
|
+
### Custom key provider
|
|
260
|
+
|
|
261
|
+
Use a key provider for KMS, Vault, or a secret manager:
|
|
262
|
+
|
|
263
|
+
```js
|
|
264
|
+
const auth = createAuthenticationLibrary({
|
|
265
|
+
jwt: {
|
|
266
|
+
keyProvider: {
|
|
267
|
+
async getActiveSigningKey() {
|
|
268
|
+
return {
|
|
269
|
+
keyId: "101",
|
|
270
|
+
privateKey: await secretManager.getActivePrivateKey()
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
async getPublicKey(keyId) {
|
|
274
|
+
return secretManager.getPublicKey(keyId);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
});
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
When `jwt.keyProvider` is supplied, the file paths are not used.
|
|
282
|
+
|
|
283
|
+
## HTTP API
|
|
284
|
+
|
|
285
|
+
Assume the router is mounted at `/auth`.
|
|
286
|
+
|
|
287
|
+
| Method and route | Request body | Description |
|
|
288
|
+
| --- | --- | --- |
|
|
289
|
+
| `POST /auth/potential-users` | `email` | Generates or refreshes a registration code for an unregistered email. |
|
|
290
|
+
| `POST /auth/register` | `email`, `password`, optional `verificationCode` | Creates a user. The code is required when verification is enabled. |
|
|
291
|
+
| `POST /auth/token` | `email`, `password`, optional `twoFactorCode` | Issues an access token and refresh-token cookie. |
|
|
292
|
+
| `POST /auth/refresh` | Optional `refresh_token` | Returns a new access token and refresh-token cookie; falls back to cookie. |
|
|
293
|
+
| `POST /auth/2fa/code` | `email`, `password` | Generates or refreshes a 2FA code when 2FA is enabled. |
|
|
294
|
+
| `PATCH /auth/password` | `newPassword`, optional `user_id` or `email` | Changes own password, or another user's if policy allows. |
|
|
295
|
+
| `POST /auth/roles` | `roles`, exactly one of `user_id` or `email` | Adds roles. Requires administrator access token. |
|
|
296
|
+
| `DELETE /auth/roles` | `role`, exactly one of `user_id` or `email` | Removes one role. Requires administrator access token. |
|
|
297
|
+
| `PUT /auth/roles` | `roles`, exactly one of `user_id` or `email` | Replaces all roles. Requires administrator access token. |
|
|
298
|
+
|
|
299
|
+
Access tokens include the application payload and access-token metadata.
|
|
300
|
+
Refresh tokens contain only the subject and required token metadata, not the
|
|
301
|
+
complete application payload.
|
|
302
|
+
|
|
303
|
+
## Data stored in MongoDB
|
|
304
|
+
|
|
305
|
+
The users collection contains documents like:
|
|
306
|
+
|
|
307
|
+
```json
|
|
308
|
+
{
|
|
309
|
+
"user_id": "2d6c2f40-0a3f-4b2e-9d8c-8f3b7f6e1a20",
|
|
310
|
+
"email": "alice@example.com",
|
|
311
|
+
"passwordHash": "<Argon2id hash>",
|
|
312
|
+
"nab_role": ["editor"],
|
|
313
|
+
"createdAt": "2026-01-01T00:00:00.000Z"
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
The library creates unique indexes for `email` and `user_id`, and creates the
|
|
318
|
+
pending-registration collection plus its TTL index.
|
|
319
|
+
|
|
320
|
+
## Verifier-only service
|
|
321
|
+
|
|
322
|
+
A business service that only verifies access tokens does not need MongoDB or a
|
|
323
|
+
private key:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
const auth = createAuthenticationLibrary({
|
|
327
|
+
jwt: {
|
|
328
|
+
issuer: "my-service",
|
|
329
|
+
audience: "my-client",
|
|
330
|
+
keys: {
|
|
331
|
+
publicKeyPath: process.env.MY_JWT_PUBLIC_KEY_PATH
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
app.get("/api/business-data", auth.requireJwt, (req, res) => {
|
|
337
|
+
res.json({ userId: req.auth.sub });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
app.use(auth.jwtErrorHandler);
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
## Production checklist
|
|
344
|
+
|
|
345
|
+
- Use HTTPS and `refreshCookie.secure: true` in production.
|
|
346
|
+
- Set `codeDelivery.allowCodeInResponseWithoutSender: false` in production.
|
|
347
|
+
- Provide real email/SMS delivery callbacks for codes.
|
|
348
|
+
- Keep private keys and MongoDB credentials outside source control.
|
|
349
|
+
- Add application-level rate limits to login, registration, code-generation,
|
|
350
|
+
and password-change routes.
|
|
351
|
+
- Configure CORS and cookie `sameSite` settings for your frontend topology.
|
|
352
|
+
- Retain old public keys until tokens signed by them expire.
|
|
353
|
+
|
|
354
|
+
## Support and contributing
|
|
355
|
+
|
|
356
|
+
Source code, bug reports, and feature requests are managed in the
|
|
357
|
+
[GitLab project](https://gitlab.com/platforms8851042/nab_authenticate).
|
|
358
|
+
Please use [GitLab Issues](https://gitlab.com/platforms8851042/nab_authenticate/-/issues)
|
|
359
|
+
for reproducible bugs and feature proposals.
|
|
360
|
+
|
|
361
|
+
## License
|
|
362
|
+
|
|
363
|
+
[MIT](LICENSE)
|
package/package.json
CHANGED
|
@@ -1,6 +1,47 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nabhiraj/nab-auth",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Configurable Express, MongoDB, JWT, registration verification, and 2FA authentication library",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/authenticationLogic/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/authenticationLogic/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src/authenticationLogic",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=20"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"authentication",
|
|
20
|
+
"express",
|
|
21
|
+
"jwt",
|
|
22
|
+
"mongodb",
|
|
23
|
+
"argon2",
|
|
24
|
+
"2fa"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"test": "node --test",
|
|
32
|
+
"pack-check": "npm pack --dry-run"
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"argon2": "^0.45.1",
|
|
36
|
+
"cookie": "^2.0.1",
|
|
37
|
+
"express-jwt": "^8.5.1",
|
|
38
|
+
"jsonwebtoken": "^9.0.2",
|
|
39
|
+
"mongodb": "^7.7.0"
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"express": "^5.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"express": "^5.1.0"
|
|
46
|
+
}
|
|
47
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { defaultAuthConfig } from "./defaultAuthConfig.js";
|
|
2
|
+
|
|
3
|
+
export function createAuthConfig(overrides = {}) {
|
|
4
|
+
const config = mergeConfig(defaultAuthConfig, overrides);
|
|
5
|
+
|
|
6
|
+
validateString(config.jwt.issuer, "jwt.issuer");
|
|
7
|
+
validateString(config.jwt.audience, "jwt.audience");
|
|
8
|
+
validateString(config.jwt.algorithm, "jwt.algorithm");
|
|
9
|
+
validatePositiveNumber(config.jwt.keyCacheTtlMs, "jwt.keyCacheTtlMs");
|
|
10
|
+
validateCodeConfig(
|
|
11
|
+
config.registration,
|
|
12
|
+
"registration",
|
|
13
|
+
"verificationRequired"
|
|
14
|
+
);
|
|
15
|
+
validateCodeConfig(config.twoFactor, "twoFactor", "required");
|
|
16
|
+
validateString(config.authorization.adminRole, "authorization.adminRole");
|
|
17
|
+
|
|
18
|
+
if (typeof config.codeDelivery.allowCodeInResponseWithoutSender !== "boolean") {
|
|
19
|
+
throw new TypeError(
|
|
20
|
+
"codeDelivery.allowCodeInResponseWithoutSender must be a boolean"
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (config.jwt.keyProvider !== undefined) {
|
|
25
|
+
if (
|
|
26
|
+
typeof config.jwt.keyProvider.getActiveSigningKey !== "function" ||
|
|
27
|
+
typeof config.jwt.keyProvider.getPublicKey !== "function"
|
|
28
|
+
) {
|
|
29
|
+
throw new TypeError(
|
|
30
|
+
"jwt.keyProvider must provide getActiveSigningKey and getPublicKey functions"
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return config;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function mergeConfig(defaults, overrides) {
|
|
39
|
+
const merged = mergeDefined(defaults, overrides);
|
|
40
|
+
|
|
41
|
+
for (const key of Object.keys(defaults)) {
|
|
42
|
+
if (isPlainObject(defaults[key])) {
|
|
43
|
+
merged[key] = mergeDefined(defaults[key], overrides[key]);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
merged.jwt.keys = mergeDefined(defaults.jwt.keys, overrides.jwt?.keys);
|
|
48
|
+
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function mergeDefined(defaults, overrides) {
|
|
53
|
+
const merged = { ...defaults };
|
|
54
|
+
|
|
55
|
+
if (!isPlainObject(overrides)) {
|
|
56
|
+
return merged;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
for (const [key, value] of Object.entries(overrides)) {
|
|
60
|
+
if (value !== undefined) {
|
|
61
|
+
merged[key] = value;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function validateCodeConfig(config, name, enabledField) {
|
|
69
|
+
if (typeof config[enabledField] !== "boolean") {
|
|
70
|
+
throw new TypeError(`${name}.${enabledField} must be a boolean`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!Number.isInteger(config.codeDigits) || config.codeDigits < 1 || config.codeDigits > 9) {
|
|
74
|
+
throw new TypeError(`${name}.codeDigits must be an integer from 1 to 9`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
validatePositiveNumber(config.codeTtlMs, `${name}.codeTtlMs`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function validateString(value, name) {
|
|
81
|
+
if (typeof value !== "string" || !value) {
|
|
82
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function validatePositiveNumber(value, name) {
|
|
87
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
|
|
88
|
+
throw new TypeError(`${name} must be a positive number`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPlainObject(value) {
|
|
93
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
94
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const defaultAuthConfig = {
|
|
2
|
+
mongo: {
|
|
3
|
+
uri: undefined,
|
|
4
|
+
databaseName: "jwt_auth_demo",
|
|
5
|
+
usersCollectionName: "users",
|
|
6
|
+
potentialUsersCollectionName: "potential_user"
|
|
7
|
+
},
|
|
8
|
+
jwt: {
|
|
9
|
+
issuer: "jwt-express-demo",
|
|
10
|
+
audience: "jwt-express-demo-client",
|
|
11
|
+
algorithm: "RS256",
|
|
12
|
+
accessTokenTtl: "15m",
|
|
13
|
+
refreshTokenTtl: "7d",
|
|
14
|
+
keyCacheTtlMs: 15 * 60 * 1000,
|
|
15
|
+
keys: {
|
|
16
|
+
privateKeyPath: undefined,
|
|
17
|
+
publicKeyPath: undefined
|
|
18
|
+
},
|
|
19
|
+
keyProvider: undefined
|
|
20
|
+
},
|
|
21
|
+
authorization: {
|
|
22
|
+
adminRole: "nab_admin",
|
|
23
|
+
userRegistrationRequiresNabAdmin: false,
|
|
24
|
+
passwordChangeRequiresNabAdmin: false,
|
|
25
|
+
nabAdminCanChangeOtherUsersPasswords: true
|
|
26
|
+
},
|
|
27
|
+
registration: {
|
|
28
|
+
verificationRequired: false,
|
|
29
|
+
codeDigits: 6,
|
|
30
|
+
codeTtlMs: 10 * 60 * 1000
|
|
31
|
+
},
|
|
32
|
+
twoFactor: {
|
|
33
|
+
required: false,
|
|
34
|
+
codeDigits: 6,
|
|
35
|
+
codeTtlMs: 10 * 60 * 1000
|
|
36
|
+
},
|
|
37
|
+
passwordHash: {
|
|
38
|
+
memoryCost: 19456,
|
|
39
|
+
timeCost: 2,
|
|
40
|
+
parallelism: 1
|
|
41
|
+
},
|
|
42
|
+
refreshCookie: {
|
|
43
|
+
name: "refresh_token",
|
|
44
|
+
httpOnly: true,
|
|
45
|
+
secure: false,
|
|
46
|
+
sameSite: "lax",
|
|
47
|
+
path: "/auth/refresh",
|
|
48
|
+
maxAgeMs: 7 * 24 * 60 * 60 * 1000
|
|
49
|
+
},
|
|
50
|
+
codeDelivery: {
|
|
51
|
+
allowCodeInResponseWithoutSender: true
|
|
52
|
+
},
|
|
53
|
+
defaultAdmin: {
|
|
54
|
+
enabled: false,
|
|
55
|
+
email: "admin@admin.com",
|
|
56
|
+
password: "admin",
|
|
57
|
+
roles: ["nab_admin"]
|
|
58
|
+
}
|
|
59
|
+
};
|