@nabhiraj/nab-auth 0.1.0 → 1.0.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/README.md +263 -197
- package/package.json +2 -2
- package/src/authenticationLogic/adapters/authStore.js +28 -0
- package/src/authenticationLogic/{repositories/userRepository.js → adapters/mongoAuthStore.js} +24 -60
- package/src/authenticationLogic/config/createAuthConfig.js +7 -0
- package/src/authenticationLogic/config/defaultAuthConfig.js +3 -5
- package/src/authenticationLogic/controllers/authController.js +17 -15
- package/src/authenticationLogic/createAuthenticationLibrary.js +35 -9
- package/src/authenticationLogic/index.js +2 -0
- package/src/authenticationLogic/services/defaultAdminService.js +27 -0
- package/src/authenticationLogic/utils/normalizeEmail.js +3 -0
package/README.md
CHANGED
|
@@ -1,52 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# nab-auth
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
and RS256 JWTs. It includes registration, password login, refresh tokens,
|
|
5
|
-
roles, optional registration verification, and optional two-factor
|
|
6
|
-
authentication.
|
|
3
|
+
[](LICENSE)
|
|
7
4
|
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
`@nabhiraj/nab-auth` is a configurable Express authentication library. It
|
|
6
|
+
provides Argon2id password hashing, RS256 access and refresh tokens, roles,
|
|
7
|
+
registration verification, and optional two-factor authentication. Storage is
|
|
8
|
+
provided by an explicit adapter: MongoDB/Atlas support is bundled, while other
|
|
9
|
+
databases can be added by implementing the auth-store contract.
|
|
10
|
+
|
|
11
|
+
The host application owns environment loading, database credentials, JWT key
|
|
12
|
+
locations, and email/SMS delivery. The library does not load `.env` files or
|
|
13
|
+
send messages itself.
|
|
14
|
+
|
|
15
|
+
## Features
|
|
16
|
+
|
|
17
|
+
- Email/password registration and login with Argon2id hashes
|
|
18
|
+
- RS256 access tokens and refresh-token cookies
|
|
19
|
+
- JWT `kid` support for public-key rotation
|
|
20
|
+
- Role claims and administrator-protected role endpoints
|
|
21
|
+
- Optional registration-code verification and two-factor authentication
|
|
22
|
+
- Explicit database adapters; MongoDB/Atlas adapter included
|
|
23
|
+
- Verifier-only mode for services that only validate access tokens
|
|
10
24
|
|
|
11
25
|
## Requirements
|
|
12
26
|
|
|
13
|
-
- Node.js 20 or
|
|
14
|
-
- Express 5
|
|
15
|
-
-
|
|
16
|
-
-
|
|
27
|
+
- Node.js 20 or newer
|
|
28
|
+
- Express 5 (peer dependency)
|
|
29
|
+
- An RSA key pair for RS256 signing and verification
|
|
30
|
+
- A database adapter for services that register users, issue tokens, or use
|
|
31
|
+
the authentication router
|
|
17
32
|
|
|
18
|
-
`argon2` is a native dependency. Most
|
|
19
|
-
|
|
33
|
+
`argon2` is a native dependency. Most platforms use a prebuilt binary; unusual
|
|
34
|
+
platforms may require standard Node.js native build tools.
|
|
20
35
|
|
|
21
36
|
## Installation
|
|
22
37
|
|
|
23
38
|
```bash
|
|
24
|
-
npm install @nabhiraj/nab-
|
|
39
|
+
npm install @nabhiraj/nab-auth express
|
|
25
40
|
```
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
## Quick start: MongoDB
|
|
28
43
|
|
|
29
|
-
|
|
44
|
+
The application loads its own environment configuration and passes the values
|
|
45
|
+
to the adapter. The library does not require a particular `.env` package or
|
|
46
|
+
variable naming convention.
|
|
30
47
|
|
|
31
48
|
```js
|
|
32
49
|
import "dotenv/config";
|
|
33
50
|
import express from "express";
|
|
34
|
-
import {
|
|
51
|
+
import {
|
|
52
|
+
createAuthenticationLibrary,
|
|
53
|
+
createMongoAuthStore
|
|
54
|
+
} from "@nabhiraj/nab-auth";
|
|
35
55
|
|
|
36
56
|
const app = express();
|
|
37
57
|
app.use(express.json());
|
|
38
58
|
|
|
39
59
|
const auth = createAuthenticationLibrary({
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
60
|
+
database: {
|
|
61
|
+
adapter: createMongoAuthStore,
|
|
62
|
+
adapterConfig: {
|
|
63
|
+
uri: process.env.MONGODB_URI,
|
|
64
|
+
databaseName: process.env.MONGODB_DB_NAME,
|
|
65
|
+
usersCollectionName: process.env.MONGODB_USERS_COLLECTION || "users",
|
|
66
|
+
potentialUsersCollectionName:
|
|
67
|
+
process.env.MONGODB_POTENTIAL_USERS_COLLECTION || "potential_users"
|
|
68
|
+
}
|
|
43
69
|
},
|
|
44
70
|
jwt: {
|
|
45
|
-
issuer:
|
|
46
|
-
audience:
|
|
71
|
+
issuer: process.env.JWT_ISSUER,
|
|
72
|
+
audience: process.env.JWT_AUDIENCE,
|
|
47
73
|
keys: {
|
|
48
|
-
privateKeyPath: process.env.
|
|
49
|
-
publicKeyPath: process.env.
|
|
74
|
+
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH,
|
|
75
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
50
76
|
}
|
|
51
77
|
},
|
|
52
78
|
defaultAdmin: { enabled: false }
|
|
@@ -61,10 +87,10 @@ app.use(
|
|
|
61
87
|
"/auth",
|
|
62
88
|
auth.createAuthRouter(createTokenPayload, {
|
|
63
89
|
sendRegistrationCode: async ({ email, verificationCode }) => {
|
|
64
|
-
await
|
|
90
|
+
await emailProvider.send(email, `Your verification code is ${verificationCode}`);
|
|
65
91
|
},
|
|
66
92
|
sendTwoFactorCode: async ({ email, verificationCode }) => {
|
|
67
|
-
await
|
|
93
|
+
await emailProvider.send(email, `Your 2FA code is ${verificationCode}`);
|
|
68
94
|
}
|
|
69
95
|
})
|
|
70
96
|
);
|
|
@@ -78,45 +104,139 @@ await auth.connectToDatabase();
|
|
|
78
104
|
app.listen(3000);
|
|
79
105
|
```
|
|
80
106
|
|
|
81
|
-
|
|
107
|
+
The order matters: call `connectToDatabase()` before receiving requests, and
|
|
108
|
+
mount `jwtErrorHandler` after routes that use `requireJwt`.
|
|
109
|
+
|
|
110
|
+
## Core API
|
|
82
111
|
|
|
83
112
|
```js
|
|
84
113
|
import {
|
|
114
|
+
assertAuthStore,
|
|
115
|
+
authStoreMethods,
|
|
85
116
|
createAuthenticationLibrary,
|
|
86
117
|
createFileJwtKeyProvider,
|
|
118
|
+
createMongoAuthStore,
|
|
87
119
|
defaultAuthConfig
|
|
88
|
-
} from "@nabhiraj/nab-
|
|
120
|
+
} from "@nabhiraj/nab-auth";
|
|
89
121
|
```
|
|
90
122
|
|
|
91
|
-
`createAuthenticationLibrary(config)` creates isolated state
|
|
92
|
-
|
|
123
|
+
`createAuthenticationLibrary(config)` creates isolated state. Its adapter,
|
|
124
|
+
JWT-key cache, middleware, and router are not shared with another library
|
|
93
125
|
instance.
|
|
94
126
|
|
|
95
127
|
| Member | Purpose |
|
|
96
128
|
| --- | --- |
|
|
97
|
-
| `connectToDatabase()` |
|
|
98
|
-
| `closeDatabase()` | Closes
|
|
99
|
-
| `createAuthRouter(payloadFactory, options)` | Creates the Express authentication router. |
|
|
100
|
-
| `requireJwt` |
|
|
101
|
-
| `requireNabAdmin` | Requires the configured administrator role
|
|
102
|
-
| `jwtErrorHandler` |
|
|
103
|
-
| `config` | The merged configuration
|
|
129
|
+
| `connectToDatabase()` | Creates the configured adapter, connects it, and creates the default admin when enabled. |
|
|
130
|
+
| `closeDatabase()` | Closes the adapter if it was created. |
|
|
131
|
+
| `createAuthRouter(payloadFactory, options)` | Creates the storage-backed Express authentication router. |
|
|
132
|
+
| `requireJwt` | Validates an access token and sets claims on `req.auth`. |
|
|
133
|
+
| `requireNabAdmin` | Requires the configured administrator role after `requireJwt`. |
|
|
134
|
+
| `jwtErrorHandler` | Converts `express-jwt` authentication failures to a `401` response. |
|
|
135
|
+
| `config` | The merged configuration for this library instance. |
|
|
136
|
+
|
|
137
|
+
`payloadFactory(user)` must return an object. Its properties are included in
|
|
138
|
+
new access tokens; the library adds `sub`, JWT metadata, and
|
|
139
|
+
`tokenType: "access"`.
|
|
140
|
+
|
|
141
|
+
## Database adapters
|
|
142
|
+
|
|
143
|
+
There is no default database. A storage-backed service must configure
|
|
144
|
+
`database.adapter` as a factory function and pass adapter-specific values in
|
|
145
|
+
`database.adapterConfig`.
|
|
146
|
+
|
|
147
|
+
```js
|
|
148
|
+
const auth = createAuthenticationLibrary({
|
|
149
|
+
database: {
|
|
150
|
+
adapter: createPostgresAuthStore,
|
|
151
|
+
adapterConfig: {
|
|
152
|
+
connectionString: process.env.DATABASE_URL,
|
|
153
|
+
usersTableName: "users",
|
|
154
|
+
pendingUsersTableName: "pending_users"
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Import the adapter in the application and pass the function itself. Do not pass
|
|
161
|
+
a source-file path or make the library dynamically load a module.
|
|
162
|
+
|
|
163
|
+
### MongoDB adapter
|
|
164
|
+
|
|
165
|
+
The package exports `createMongoAuthStore`. Its adapter configuration is:
|
|
166
|
+
|
|
167
|
+
| Field | Required | Description |
|
|
168
|
+
| --- | --- | --- |
|
|
169
|
+
| `uri` | Yes | MongoDB or MongoDB Atlas connection URI. |
|
|
170
|
+
| `databaseName` | Yes | Database used exclusively for auth data. |
|
|
171
|
+
| `usersCollectionName` | Yes | Collection for permanent user records. |
|
|
172
|
+
| `potentialUsersCollectionName` | Yes | Collection for pending registration records. |
|
|
173
|
+
|
|
174
|
+
On `connect()`, the Mongo adapter creates missing collections and indexes:
|
|
175
|
+
|
|
176
|
+
- Unique indexes for `users.email` and `users.user_id`
|
|
177
|
+
- A unique index for pending-user `email`
|
|
178
|
+
- A TTL index on pending-user `expiresAt`
|
|
179
|
+
|
|
180
|
+
It also migrates existing Mongo user records that lack `user_id` or `nab_role`.
|
|
181
|
+
|
|
182
|
+
### Custom adapter contract
|
|
183
|
+
|
|
184
|
+
An adapter factory receives `adapterConfig` and returns an object that
|
|
185
|
+
implements every method below. `assertAuthStore(store)` validates the shape,
|
|
186
|
+
and `authStoreMethods` exports the method names.
|
|
187
|
+
|
|
188
|
+
| Method | Required behavior |
|
|
189
|
+
| --- | --- |
|
|
190
|
+
| `connect()` | Open the database connection and prepare storage. |
|
|
191
|
+
| `close()` | Release the database connection/resources. |
|
|
192
|
+
| `findUserByEmail(email)` | Return the user record or `null`. |
|
|
193
|
+
| `findUserByUserId(userId)` | Return the user record or `null`. |
|
|
194
|
+
| `createUser(user)` | Persist the user and return `true`; return `false` when its email already exists. |
|
|
195
|
+
| `upsertPotentialUser(email, hash, generatedAt, expiresAt)` | Create/update the pending registration; return `false` when the email is already registered. |
|
|
196
|
+
| `findPotentialUserByEmail(email)` | Return the pending record or `null`. |
|
|
197
|
+
| `deletePotentialUserByEmail(email)` | Delete the pending registration if present. |
|
|
198
|
+
| `updateUserPassword(userId, hash)` | Update the password hash; return whether a user matched. |
|
|
199
|
+
| `updateUserTwoFactorCode(userId, hash, generatedAt, expiresAt)` | Store a new 2FA code hash; return whether a user matched. |
|
|
200
|
+
| `consumeUserTwoFactorCode(userId, hash)` | Atomically consume exactly that unused 2FA hash; return whether it was consumed. |
|
|
201
|
+
| `addUserRoles(userId, roles)` | Add unique roles and return the updated user or `null`. |
|
|
202
|
+
| `removeUserRole(userId, role)` | Remove a role and return the updated user or `null`. |
|
|
203
|
+
| `replaceUserRoles(userId, roles)` | Replace all roles and return the updated user or `null`. |
|
|
204
|
+
|
|
205
|
+
User records returned by an adapter must provide at least:
|
|
104
206
|
|
|
105
|
-
|
|
106
|
-
|
|
207
|
+
```js
|
|
208
|
+
{
|
|
209
|
+
user_id: "uuid",
|
|
210
|
+
email: "person@example.com",
|
|
211
|
+
passwordHash: "argon2 hash",
|
|
212
|
+
nab_role: ["role"],
|
|
213
|
+
createdAt: new Date()
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Pending records must provide `verificationCodeHash` and `codeExpiresAt` as a
|
|
218
|
+
JavaScript `Date`. User records with a pending 2FA challenge use
|
|
219
|
+
`twoFactorCodeHash` and `twoFactorCodeExpiresAt`.
|
|
220
|
+
|
|
221
|
+
The core normalizes email before it invokes the store. Custom adapters should
|
|
222
|
+
preserve that normalized email value and enforce unique email storage.
|
|
107
223
|
|
|
108
224
|
## Configuration
|
|
109
225
|
|
|
110
|
-
|
|
111
|
-
`
|
|
226
|
+
All settings use `defaultAuthConfig` when omitted. `database.adapter` is
|
|
227
|
+
required before calling `connectToDatabase()` or `createAuthRouter()`. A
|
|
228
|
+
verifier-only service can omit it.
|
|
112
229
|
|
|
113
230
|
```js
|
|
114
231
|
const auth = createAuthenticationLibrary({
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
232
|
+
database: {
|
|
233
|
+
adapter: createMongoAuthStore,
|
|
234
|
+
adapterConfig: {
|
|
235
|
+
uri: process.env.MONGODB_URI,
|
|
236
|
+
databaseName: process.env.MONGODB_DB_NAME,
|
|
237
|
+
usersCollectionName: "users",
|
|
238
|
+
potentialUsersCollectionName: "potential_users"
|
|
239
|
+
}
|
|
120
240
|
},
|
|
121
241
|
jwt: {
|
|
122
242
|
issuer: "my-service",
|
|
@@ -126,8 +246,8 @@ const auth = createAuthenticationLibrary({
|
|
|
126
246
|
refreshTokenTtl: "7d",
|
|
127
247
|
keyCacheTtlMs: 15 * 60 * 1000,
|
|
128
248
|
keys: {
|
|
129
|
-
privateKeyPath: process.env.
|
|
130
|
-
publicKeyPath: process.env.
|
|
249
|
+
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH,
|
|
250
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
131
251
|
}
|
|
132
252
|
},
|
|
133
253
|
authorization: {
|
|
@@ -146,11 +266,6 @@ const auth = createAuthenticationLibrary({
|
|
|
146
266
|
codeDigits: 6,
|
|
147
267
|
codeTtlMs: 10 * 60 * 1000
|
|
148
268
|
},
|
|
149
|
-
passwordHash: {
|
|
150
|
-
memoryCost: 19456,
|
|
151
|
-
timeCost: 2,
|
|
152
|
-
parallelism: 1
|
|
153
|
-
},
|
|
154
269
|
refreshCookie: {
|
|
155
270
|
name: "refresh_token",
|
|
156
271
|
httpOnly: true,
|
|
@@ -164,57 +279,55 @@ const auth = createAuthenticationLibrary({
|
|
|
164
279
|
},
|
|
165
280
|
defaultAdmin: {
|
|
166
281
|
enabled: false,
|
|
167
|
-
email: "admin@
|
|
168
|
-
password: "
|
|
282
|
+
email: "admin@example.com",
|
|
283
|
+
password: "use-a-strong-secret",
|
|
169
284
|
roles: ["nab_admin"]
|
|
170
285
|
}
|
|
171
286
|
});
|
|
172
287
|
```
|
|
173
288
|
|
|
174
|
-
|
|
175
|
-
to `
|
|
176
|
-
`false`;
|
|
289
|
+
`refreshCookie.secure` defaults to `false` for local HTTP development; set it
|
|
290
|
+
to `true` for HTTPS production deployments. `defaultAdmin.enabled` defaults to
|
|
291
|
+
`false`; do not enable it with a known or weak password.
|
|
292
|
+
|
|
293
|
+
### Registration and authorization policy
|
|
294
|
+
|
|
295
|
+
| Setting | Effect |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| `registration.verificationRequired` | Requires a valid registration code on `POST /register`. |
|
|
298
|
+
| `twoFactor.required` | Requires a valid 2FA code before issuing tokens. |
|
|
299
|
+
| `authorization.userRegistrationRequiresNabAdmin` | Requires a JWT-bearing administrator to register users. |
|
|
300
|
+
| `authorization.passwordChangeRequiresNabAdmin` | Requires a JWT-bearing administrator for all password changes. |
|
|
301
|
+
| `authorization.nabAdminCanChangeOtherUsersPasswords` | Lets an administrator change another user’s password when admin-only password changes are disabled. |
|
|
177
302
|
|
|
178
303
|
### Code delivery callbacks
|
|
179
304
|
|
|
305
|
+
Supply callbacks only if the associated feature is enabled:
|
|
306
|
+
|
|
180
307
|
```js
|
|
181
308
|
auth.createAuthRouter(createTokenPayload, {
|
|
182
|
-
sendRegistrationCode: async ({
|
|
183
|
-
email,
|
|
184
|
-
verificationCode,
|
|
185
|
-
codeGeneratedAt,
|
|
186
|
-
codeExpiresAt
|
|
187
|
-
}) => {
|
|
309
|
+
sendRegistrationCode: async ({ email, verificationCode }) => {
|
|
188
310
|
await emailProvider.send(email, verificationCode);
|
|
189
311
|
},
|
|
190
|
-
sendTwoFactorCode: async ({
|
|
191
|
-
email,
|
|
192
|
-
verificationCode,
|
|
193
|
-
codeGeneratedAt,
|
|
194
|
-
codeExpiresAt
|
|
195
|
-
}) => {
|
|
312
|
+
sendTwoFactorCode: async ({ email, verificationCode }) => {
|
|
196
313
|
await smsProvider.send(email, verificationCode);
|
|
197
314
|
}
|
|
198
315
|
});
|
|
199
316
|
```
|
|
200
317
|
|
|
201
|
-
`codeDelivery.allowCodeInResponseWithoutSender` controls missing
|
|
318
|
+
`codeDelivery.allowCodeInResponseWithoutSender` controls a missing callback:
|
|
202
319
|
|
|
203
|
-
- `true
|
|
320
|
+
- `true`: returns the generated code in the HTTP response. Use only for local
|
|
204
321
|
development or automated tests.
|
|
205
|
-
- `false
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
Registration codes are stored as Argon2id hashes in `potential_user` and are
|
|
209
|
-
removed by a MongoDB TTL index. 2FA code hashes are stored on the user and are
|
|
210
|
-
atomically consumed after a successful token issue.
|
|
322
|
+
- `false`: the route fails when a required callback is absent. Use this in
|
|
323
|
+
production.
|
|
211
324
|
|
|
212
|
-
## JWT
|
|
325
|
+
## JWT keys and rotation
|
|
213
326
|
|
|
214
|
-
The built-in key provider loads JSON files lazily and
|
|
327
|
+
The built-in file key provider loads JSON files lazily and caches them for
|
|
215
328
|
`jwt.keyCacheTtlMs`.
|
|
216
329
|
|
|
217
|
-
Private-key JSON
|
|
330
|
+
Private-key JSON contains the active signing key:
|
|
218
331
|
|
|
219
332
|
```json
|
|
220
333
|
{
|
|
@@ -223,7 +336,7 @@ Private-key JSON has one active signing key:
|
|
|
223
336
|
}
|
|
224
337
|
```
|
|
225
338
|
|
|
226
|
-
Public-key JSON
|
|
339
|
+
Public-key JSON contains every currently valid verification key:
|
|
227
340
|
|
|
228
341
|
```json
|
|
229
342
|
{
|
|
@@ -239,84 +352,67 @@ Public-key JSON keeps each currently valid verification key:
|
|
|
239
352
|
Rotation process:
|
|
240
353
|
|
|
241
354
|
1. Add the new public key and `kid` to the public-key JSON.
|
|
242
|
-
2. Switch the
|
|
243
|
-
3. Keep the
|
|
244
|
-
|
|
245
|
-
Never commit or publish private keys.
|
|
355
|
+
2. Switch the private-key JSON to the matching active key and `kid`.
|
|
356
|
+
3. Keep the old public key until tokens signed by it have expired.
|
|
246
357
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
Use a key provider for KMS, Vault, or a secret manager:
|
|
358
|
+
Never commit private keys. For KMS, Vault, or another secret manager, provide
|
|
359
|
+
`jwt.keyProvider` instead:
|
|
250
360
|
|
|
251
361
|
```js
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return secretManager.getPublicKey(keyId);
|
|
263
|
-
}
|
|
362
|
+
jwt: {
|
|
363
|
+
keyProvider: {
|
|
364
|
+
async getActiveSigningKey() {
|
|
365
|
+
return {
|
|
366
|
+
keyId: "101",
|
|
367
|
+
privateKey: await secretManager.getActivePrivateKey()
|
|
368
|
+
};
|
|
369
|
+
},
|
|
370
|
+
async getPublicKey(keyId) {
|
|
371
|
+
return secretManager.getPublicKey(keyId);
|
|
264
372
|
}
|
|
265
373
|
}
|
|
266
|
-
}
|
|
374
|
+
}
|
|
267
375
|
```
|
|
268
376
|
|
|
269
|
-
When `jwt.keyProvider` is
|
|
377
|
+
When `jwt.keyProvider` is present, `jwt.keys` is not used.
|
|
270
378
|
|
|
271
|
-
##
|
|
379
|
+
## HTTP API
|
|
272
380
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
| Method and route | Request body | Description |
|
|
276
|
-
| --- | --- | --- |
|
|
277
|
-
| `POST /auth/potential-users` | `email` | Generates or refreshes a registration code for an unregistered email. |
|
|
278
|
-
| `POST /auth/register` | `email`, `password`, optional `verificationCode` | Creates a user. The code is required when verification is enabled. |
|
|
279
|
-
| `POST /auth/token` | `email`, `password`, optional `twoFactorCode` | Issues an access token and refresh-token cookie. |
|
|
280
|
-
| `POST /auth/refresh` | Optional `refresh_token` | Returns a new access token and refresh-token cookie; falls back to cookie. |
|
|
281
|
-
| `POST /auth/2fa/code` | `email`, `password` | Generates or refreshes a 2FA code when 2FA is enabled. |
|
|
282
|
-
| `PATCH /auth/password` | `newPassword`, optional `user_id` or `email` | Changes own password, or another user's if policy allows. |
|
|
283
|
-
| `POST /auth/roles` | `roles`, exactly one of `user_id` or `email` | Adds roles. Requires administrator access token. |
|
|
284
|
-
| `DELETE /auth/roles` | `role`, exactly one of `user_id` or `email` | Removes one role. Requires administrator access token. |
|
|
285
|
-
| `PUT /auth/roles` | `roles`, exactly one of `user_id` or `email` | Replaces all roles. Requires administrator access token. |
|
|
381
|
+
Mount the router at `/auth` to use these routes:
|
|
286
382
|
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
383
|
+
| Method and route | Request body | Success | Notes |
|
|
384
|
+
| --- | --- | --- | --- |
|
|
385
|
+
| `POST /auth/potential-users` | `email` | `201` or `202` | Starts/refreshes registration verification. `202` is used when a sender callback is configured. |
|
|
386
|
+
| `POST /auth/register` | `email`, `password`, optional `verificationCode` | `201` | Code required when registration verification is enabled. |
|
|
387
|
+
| `POST /auth/token` | `email`, `password`, optional `twoFactorCode` | `200` | Returns access token and sets refresh-token cookie. |
|
|
388
|
+
| `POST /auth/refresh` | Optional `refresh_token` | `200` | Uses body token first, then the configured cookie. Issues a new refresh token. |
|
|
389
|
+
| `POST /auth/2fa/code` | `email`, `password` | `201` or `202` | Only useful when 2FA is enabled. |
|
|
390
|
+
| `PATCH /auth/password` | `newPassword`, optional `user_id` or `email` | `204` | Requires JWT; authorization settings can require admin access. |
|
|
391
|
+
| `POST /auth/roles` | `roles`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
392
|
+
| `DELETE /auth/roles` | `role`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
393
|
+
| `PUT /auth/roles` | `roles`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
290
394
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
```json
|
|
296
|
-
{
|
|
297
|
-
"user_id": "2d6c2f40-0a3f-4b2e-9d8c-8f3b7f6e1a20",
|
|
298
|
-
"email": "alice@example.com",
|
|
299
|
-
"passwordHash": "<Argon2id hash>",
|
|
300
|
-
"nab_role": ["editor"],
|
|
301
|
-
"createdAt": "2026-01-01T00:00:00.000Z"
|
|
302
|
-
}
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
The library creates unique indexes for `email` and `user_id`, and creates the
|
|
306
|
-
pending-registration collection plus its TTL index.
|
|
395
|
+
Access tokens include the application payload. Refresh tokens contain only the
|
|
396
|
+
subject and token metadata. The library does not persist refresh tokens or
|
|
397
|
+
revoke previously issued refresh tokens; the host application should add a
|
|
398
|
+
token-revocation strategy if that is required.
|
|
307
399
|
|
|
308
400
|
## Verifier-only service
|
|
309
401
|
|
|
310
|
-
A
|
|
402
|
+
A service that only validates access tokens needs no database adapter and no
|
|
311
403
|
private key:
|
|
312
404
|
|
|
313
405
|
```js
|
|
406
|
+
import express from "express";
|
|
407
|
+
import { createAuthenticationLibrary } from "@nabhiraj/nab-auth";
|
|
408
|
+
|
|
409
|
+
const app = express();
|
|
314
410
|
const auth = createAuthenticationLibrary({
|
|
315
411
|
jwt: {
|
|
316
412
|
issuer: "my-service",
|
|
317
413
|
audience: "my-client",
|
|
318
414
|
keys: {
|
|
319
|
-
publicKeyPath: process.env.
|
|
415
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
320
416
|
}
|
|
321
417
|
}
|
|
322
418
|
});
|
|
@@ -328,76 +424,46 @@ app.get("/api/business-data", auth.requireJwt, (req, res) => {
|
|
|
328
424
|
app.use(auth.jwtErrorHandler);
|
|
329
425
|
```
|
|
330
426
|
|
|
331
|
-
|
|
427
|
+
Do not call `connectToDatabase()` or `createAuthRouter()` in this mode.
|
|
332
428
|
|
|
333
|
-
|
|
334
|
-
- Set `codeDelivery.allowCodeInResponseWithoutSender: false` in production.
|
|
335
|
-
- Provide real email/SMS delivery callbacks for codes.
|
|
336
|
-
- Keep private keys and MongoDB credentials outside source control.
|
|
337
|
-
- Add application-level rate limits to login, registration, code-generation,
|
|
338
|
-
and password-change routes.
|
|
339
|
-
- Configure CORS and cookie `sameSite` settings for your frontend topology.
|
|
340
|
-
- Retain old public keys until tokens signed by them expire.
|
|
429
|
+
## Testing
|
|
341
430
|
|
|
342
|
-
|
|
431
|
+
Run unit and adapter-contract tests:
|
|
343
432
|
|
|
344
433
|
```bash
|
|
345
|
-
npm install
|
|
346
434
|
npm test
|
|
347
435
|
```
|
|
348
436
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
install and load their example-only dependencies separately.
|
|
352
|
-
|
|
353
|
-
Run automated tests:
|
|
354
|
-
|
|
355
|
-
```bash
|
|
356
|
-
npm test
|
|
357
|
-
```
|
|
358
|
-
|
|
359
|
-
Before publishing, inspect the exact package contents:
|
|
360
|
-
|
|
361
|
-
```bash
|
|
362
|
-
npm run pack-check
|
|
363
|
-
```
|
|
364
|
-
|
|
365
|
-
Test the generated archive from a separate consumer project:
|
|
366
|
-
|
|
367
|
-
```bash
|
|
368
|
-
npm pack
|
|
369
|
-
mkdir ../auth-package-consumer
|
|
370
|
-
cd ../auth-package-consumer
|
|
371
|
-
npm init -y
|
|
372
|
-
npm install ../nab_authenticate/nabhiraj-nab-authentication-0.1.0.tgz express
|
|
373
|
-
```
|
|
374
|
-
|
|
375
|
-
Create a small Express app there and verify import, database connection,
|
|
376
|
-
router creation, and JWT verification.
|
|
377
|
-
|
|
378
|
-
## Publishing
|
|
379
|
-
|
|
380
|
-
1. Create an npm account and check that the package name is available.
|
|
381
|
-
2. Run `npm test` and `npm run pack-check`.
|
|
382
|
-
3. Confirm that `.env`, key files, and demo applications are not in the pack
|
|
383
|
-
listing.
|
|
384
|
-
4. Authenticate with `npm login`.
|
|
385
|
-
5. Publish:
|
|
386
|
-
|
|
387
|
-
```bash
|
|
388
|
-
npm publish
|
|
389
|
-
```
|
|
390
|
-
|
|
391
|
-
For later releases:
|
|
437
|
+
Run the opt-in live MongoDB end-to-end suite only with dedicated test
|
|
438
|
+
credentials and an isolated test database:
|
|
392
439
|
|
|
393
440
|
```bash
|
|
394
|
-
npm
|
|
395
|
-
npm version minor # 0.1.0 -> 0.2.0
|
|
396
|
-
npm version major # 0.1.0 -> 1.0.0
|
|
397
|
-
npm publish
|
|
441
|
+
npm run test:e2e:mongo
|
|
398
442
|
```
|
|
399
443
|
|
|
400
|
-
|
|
444
|
+
See [E2E_TEST_PLAN.md](E2E_TEST_PLAN.md) for required endpoint and
|
|
445
|
+
configuration coverage. Never point live tests at the host application’s
|
|
446
|
+
database.
|
|
447
|
+
|
|
448
|
+
## Production checklist
|
|
449
|
+
|
|
450
|
+
- Keep database credentials and private keys outside source control.
|
|
451
|
+
- Use HTTPS and `refreshCookie.secure: true`.
|
|
452
|
+
- Set `codeDelivery.allowCodeInResponseWithoutSender: false`.
|
|
453
|
+
- Provide real code-delivery callbacks for enabled verification features.
|
|
454
|
+
- Add application-level rate limits to login, registration, code generation,
|
|
455
|
+
refresh, and password-change routes.
|
|
456
|
+
- Configure CORS and `sameSite` for the frontend topology.
|
|
457
|
+
- Retain old public keys until every token signed with them expires.
|
|
458
|
+
- Define refresh-token revocation if logout, breach response, or session
|
|
459
|
+
invalidation requires it.
|
|
460
|
+
|
|
461
|
+
## Support and contributing
|
|
462
|
+
|
|
463
|
+
Source code, bug reports, and feature requests are managed in the
|
|
464
|
+
[GitLab project](https://gitlab.com/platforms8851042/nab_authenticate).
|
|
465
|
+
Please use [GitLab Issues](https://gitlab.com/platforms8851042/nab_authenticate/-/issues)
|
|
466
|
+
for reproducible bugs and feature proposals.
|
|
401
467
|
|
|
402
468
|
## License
|
|
403
469
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nabhiraj/nab-auth",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Configurable Express
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Configurable Express authentication library with pluggable database adapters, JWT, registration verification, and 2FA",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/authenticationLogic/index.js",
|
|
7
7
|
"exports": {
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export const authStoreMethods = [
|
|
2
|
+
"connect",
|
|
3
|
+
"close",
|
|
4
|
+
"findUserByEmail",
|
|
5
|
+
"findUserByUserId",
|
|
6
|
+
"createUser",
|
|
7
|
+
"upsertPotentialUser",
|
|
8
|
+
"findPotentialUserByEmail",
|
|
9
|
+
"deletePotentialUserByEmail",
|
|
10
|
+
"updateUserPassword",
|
|
11
|
+
"updateUserTwoFactorCode",
|
|
12
|
+
"consumeUserTwoFactorCode",
|
|
13
|
+
"addUserRoles",
|
|
14
|
+
"removeUserRole",
|
|
15
|
+
"replaceUserRoles"
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
export function assertAuthStore(store) {
|
|
19
|
+
if (!store || typeof store !== "object") {
|
|
20
|
+
throw new TypeError("database.adapter must be an auth store object");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
for (const method of authStoreMethods) {
|
|
24
|
+
if (typeof store[method] !== "function") {
|
|
25
|
+
throw new TypeError(`database.adapter must provide a ${method} function`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
package/src/authenticationLogic/{repositories/userRepository.js → adapters/mongoAuthStore.js}
RENAMED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import argon2 from "argon2";
|
|
3
2
|
import { MongoClient } from "mongodb";
|
|
3
|
+
import { normalizeEmail } from "../utils/normalizeEmail.js";
|
|
4
4
|
|
|
5
|
-
export function
|
|
5
|
+
export function createMongoAuthStore(mongo) {
|
|
6
6
|
let client;
|
|
7
7
|
let usersCollection;
|
|
8
8
|
let potentialUsersCollection;
|
|
@@ -29,57 +29,10 @@ export function createUserRepository({ mongo, defaultAdmin, passwordHash }) {
|
|
|
29
29
|
return usersCollection.findOne({ user_id: userId });
|
|
30
30
|
}
|
|
31
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
32
|
return {
|
|
80
|
-
async
|
|
33
|
+
async connect() {
|
|
81
34
|
if (usersCollection) {
|
|
82
|
-
return
|
|
35
|
+
return;
|
|
83
36
|
}
|
|
84
37
|
|
|
85
38
|
if (typeof mongo.uri !== "string" || !mongo.uri) {
|
|
@@ -107,12 +60,9 @@ export function createUserRepository({ mongo, defaultAdmin, passwordHash }) {
|
|
|
107
60
|
{ expiresAt: 1 },
|
|
108
61
|
{ expireAfterSeconds: 0 }
|
|
109
62
|
);
|
|
110
|
-
await ensureDefaultAdminUser();
|
|
111
|
-
|
|
112
|
-
return usersCollection;
|
|
113
63
|
},
|
|
114
64
|
|
|
115
|
-
async
|
|
65
|
+
async close() {
|
|
116
66
|
usersCollection = undefined;
|
|
117
67
|
potentialUsersCollection = undefined;
|
|
118
68
|
|
|
@@ -124,7 +74,25 @@ export function createUserRepository({ mongo, defaultAdmin, passwordHash }) {
|
|
|
124
74
|
|
|
125
75
|
findUserByEmail,
|
|
126
76
|
findUserByUserId,
|
|
127
|
-
|
|
77
|
+
|
|
78
|
+
async createUser(user) {
|
|
79
|
+
requireUsersCollection();
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
await usersCollection.insertOne({
|
|
83
|
+
...user,
|
|
84
|
+
email: normalizeEmail(user.email),
|
|
85
|
+
nab_role: user.nab_role || [],
|
|
86
|
+
createdAt: user.createdAt || new Date()
|
|
87
|
+
});
|
|
88
|
+
return true;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (error.code === 11000) {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
128
96
|
|
|
129
97
|
async upsertPotentialUser(
|
|
130
98
|
email,
|
|
@@ -275,7 +243,3 @@ async function updateUserRoles(usersCollection, userId, update) {
|
|
|
275
243
|
|
|
276
244
|
return usersCollection.findOne({ user_id: userId });
|
|
277
245
|
}
|
|
278
|
-
|
|
279
|
-
export function normalizeEmail(email) {
|
|
280
|
-
return email.trim().toLowerCase();
|
|
281
|
-
}
|
|
@@ -15,6 +15,13 @@ export function createAuthConfig(overrides = {}) {
|
|
|
15
15
|
validateCodeConfig(config.twoFactor, "twoFactor", "required");
|
|
16
16
|
validateString(config.authorization.adminRole, "authorization.adminRole");
|
|
17
17
|
|
|
18
|
+
if (
|
|
19
|
+
config.database.adapter !== undefined &&
|
|
20
|
+
typeof config.database.adapter !== "function"
|
|
21
|
+
) {
|
|
22
|
+
throw new TypeError("database.adapter must be an adapter factory function");
|
|
23
|
+
}
|
|
24
|
+
|
|
18
25
|
if (typeof config.codeDelivery.allowCodeInResponseWithoutSender !== "boolean") {
|
|
19
26
|
throw new TypeError(
|
|
20
27
|
"codeDelivery.allowCodeInResponseWithoutSender must be a boolean"
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
export const defaultAuthConfig = {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
usersCollectionName: "users",
|
|
6
|
-
potentialUsersCollectionName: "potential_user"
|
|
2
|
+
database: {
|
|
3
|
+
adapter: undefined,
|
|
4
|
+
adapterConfig: undefined
|
|
7
5
|
},
|
|
8
6
|
jwt: {
|
|
9
7
|
issuer: "jwt-express-demo",
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import argon2 from "argon2";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { parseCookie } from "cookie";
|
|
3
|
-
import { normalizeEmail } from "../
|
|
4
|
+
import { normalizeEmail } from "../utils/normalizeEmail.js";
|
|
4
5
|
import {
|
|
5
6
|
generateNumericVerificationCode,
|
|
6
7
|
hashVerificationCode,
|
|
@@ -170,23 +171,24 @@ export function createAuthController({
|
|
|
170
171
|
}
|
|
171
172
|
}
|
|
172
173
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
174
|
+
const userId = randomUUID();
|
|
175
|
+
const userCreated = await repository.createUser({
|
|
176
|
+
user_id: userId,
|
|
177
|
+
email: normalizedEmail,
|
|
178
|
+
passwordHash: await hashPassword(password, config.passwordHash),
|
|
179
|
+
nab_role: [],
|
|
180
|
+
createdAt: new Date()
|
|
181
|
+
});
|
|
178
182
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
183
|
+
if (!userCreated) {
|
|
184
|
+
return res.status(409).json({ message: "User already exists" });
|
|
185
|
+
}
|
|
182
186
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (error.code === 11000) {
|
|
186
|
-
return res.status(409).json({ message: "User already exists" });
|
|
187
|
-
}
|
|
188
|
-
throw error;
|
|
187
|
+
if (config.registration.verificationRequired) {
|
|
188
|
+
await repository.deletePotentialUserByEmail(normalizedEmail);
|
|
189
189
|
}
|
|
190
|
+
|
|
191
|
+
return res.status(201).json({ user_id: userId, email: normalizedEmail });
|
|
190
192
|
} catch (error) {
|
|
191
193
|
next(error);
|
|
192
194
|
}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import { createAuthConfig } from "./config/createAuthConfig.js";
|
|
2
2
|
import { createAuthController } from "./controllers/authController.js";
|
|
3
3
|
import { createJwtMiddleware } from "./middleware/jwtMiddleware.js";
|
|
4
|
-
import {
|
|
4
|
+
import { assertAuthStore } from "./adapters/authStore.js";
|
|
5
5
|
import { createAuthRouter } from "./routes/authRoutes.js";
|
|
6
|
+
import { ensureDefaultAdminUser } from "./services/defaultAdminService.js";
|
|
6
7
|
import { createJwtKeyProvider } from "./services/jwtKeyProvider.js";
|
|
7
8
|
import { createTokenService } from "./services/tokenService.js";
|
|
8
9
|
|
|
9
10
|
export function createAuthenticationLibrary(configOverrides = {}) {
|
|
10
11
|
const config = createAuthConfig(configOverrides);
|
|
11
12
|
const keyProvider = createJwtKeyProvider(config.jwt);
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
let repository;
|
|
14
|
+
|
|
15
|
+
function getRepository() {
|
|
16
|
+
if (repository) {
|
|
17
|
+
return repository;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof config.database.adapter !== "function") {
|
|
21
|
+
throw new Error(
|
|
22
|
+
"database.adapter must be configured before connecting to a database or creating an authentication router"
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
repository = config.database.adapter(config.database.adapterConfig);
|
|
27
|
+
assertAuthStore(repository);
|
|
28
|
+
return repository;
|
|
29
|
+
}
|
|
30
|
+
|
|
17
31
|
const tokenService = createTokenService({ jwt: config.jwt, keyProvider });
|
|
18
32
|
const middleware = createJwtMiddleware({
|
|
19
33
|
jwt: config.jwt,
|
|
@@ -23,15 +37,27 @@ export function createAuthenticationLibrary(configOverrides = {}) {
|
|
|
23
37
|
|
|
24
38
|
return {
|
|
25
39
|
config,
|
|
26
|
-
connectToDatabase
|
|
27
|
-
|
|
40
|
+
async connectToDatabase() {
|
|
41
|
+
const store = getRepository();
|
|
42
|
+
await store.connect();
|
|
43
|
+
await ensureDefaultAdminUser({
|
|
44
|
+
store,
|
|
45
|
+
defaultAdmin: config.defaultAdmin,
|
|
46
|
+
passwordHash: config.passwordHash
|
|
47
|
+
});
|
|
48
|
+
},
|
|
49
|
+
async closeDatabase() {
|
|
50
|
+
if (repository) {
|
|
51
|
+
await repository.close();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
28
54
|
requireJwt: middleware.requireJwt,
|
|
29
55
|
requireNabAdmin: middleware.requireNabAdmin,
|
|
30
56
|
jwtErrorHandler: middleware.jwtErrorHandler,
|
|
31
57
|
createAuthRouter(payloadFactory, options = {}) {
|
|
32
58
|
const controller = createAuthController({
|
|
33
59
|
config,
|
|
34
|
-
repository,
|
|
60
|
+
repository: getRepository(),
|
|
35
61
|
tokenService,
|
|
36
62
|
payloadFactory,
|
|
37
63
|
options
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { createAuthenticationLibrary } from "./createAuthenticationLibrary.js";
|
|
2
2
|
export { defaultAuthConfig } from "./config/defaultAuthConfig.js";
|
|
3
3
|
export { createFileJwtKeyProvider } from "./services/jwtKeyProvider.js";
|
|
4
|
+
export { createMongoAuthStore } from "./adapters/mongoAuthStore.js";
|
|
5
|
+
export { authStoreMethods, assertAuthStore } from "./adapters/authStore.js";
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import argon2 from "argon2";
|
|
3
|
+
import { normalizeEmail } from "../utils/normalizeEmail.js";
|
|
4
|
+
|
|
5
|
+
export async function ensureDefaultAdminUser({ store, defaultAdmin, passwordHash }) {
|
|
6
|
+
if (!defaultAdmin.enabled) {
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
const email = normalizeEmail(defaultAdmin.email);
|
|
11
|
+
if (await store.findUserByEmail(email)) {
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const passwordHashValue = await argon2.hash(defaultAdmin.password, {
|
|
16
|
+
type: argon2.argon2id,
|
|
17
|
+
...passwordHash
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
await store.createUser({
|
|
21
|
+
user_id: randomUUID(),
|
|
22
|
+
email,
|
|
23
|
+
passwordHash: passwordHashValue,
|
|
24
|
+
nab_role: defaultAdmin.roles,
|
|
25
|
+
createdAt: new Date()
|
|
26
|
+
});
|
|
27
|
+
}
|