@nabhiraj/nab-auth 0.1.1 → 1.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/README.md +302 -146
- 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 +12 -0
- package/src/authenticationLogic/config/defaultAuthConfig.js +4 -5
- package/src/authenticationLogic/controllers/authController.js +27 -15
- package/src/authenticationLogic/createAuthenticationLibrary.js +36 -10
- package/src/authenticationLogic/index.js +2 -0
- package/src/authenticationLogic/routes/authRoutes.js +45 -22
- package/src/authenticationLogic/services/defaultAdminService.js +27 -0
- package/src/authenticationLogic/utils/normalizeEmail.js +3 -0
package/README.md
CHANGED
|
@@ -1,33 +1,39 @@
|
|
|
1
1
|
# nab-auth
|
|
2
2
|
|
|
3
|
+
<img src="./nab-auth-logo.png" alt="nab-auth logo" width="180">
|
|
4
|
+
|
|
3
5
|
[](LICENSE)
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
Argon2id password hashing, RS256
|
|
7
|
-
registration verification, and optional two-factor authentication.
|
|
7
|
+
`@nabhiraj/nab-auth` is a configurable Express authentication library. It
|
|
8
|
+
provides Argon2id password hashing, RS256 access and refresh tokens, roles,
|
|
9
|
+
registration verification, and optional two-factor authentication. Storage is
|
|
10
|
+
provided by an explicit adapter: MongoDB/Atlas support is bundled, while other
|
|
11
|
+
databases can be added by implementing the auth-store contract.
|
|
8
12
|
|
|
9
|
-
The
|
|
10
|
-
|
|
13
|
+
The host application owns environment loading, database credentials, JWT key
|
|
14
|
+
locations, and email/SMS delivery. The library does not load `.env` files or
|
|
15
|
+
send messages itself.
|
|
11
16
|
|
|
12
17
|
## Features
|
|
13
18
|
|
|
14
|
-
- Email/password registration and login with Argon2id
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
- Role claims and administrator-protected role
|
|
19
|
+
- Email/password registration and login with Argon2id hashes
|
|
20
|
+
- RS256 access tokens and refresh-token cookies
|
|
21
|
+
- JWT `kid` support for public-key rotation
|
|
22
|
+
- Role claims and administrator-protected role endpoints
|
|
18
23
|
- Optional registration-code verification and two-factor authentication
|
|
19
|
-
- MongoDB/Atlas
|
|
20
|
-
- Verifier-only mode for services that only
|
|
24
|
+
- Explicit database adapters; MongoDB/Atlas adapter included
|
|
25
|
+
- Verifier-only mode for services that only validate access tokens
|
|
21
26
|
|
|
22
27
|
## Requirements
|
|
23
28
|
|
|
24
|
-
- Node.js 20 or
|
|
25
|
-
- Express 5
|
|
26
|
-
-
|
|
27
|
-
-
|
|
29
|
+
- Node.js 20 or newer
|
|
30
|
+
- Express 5 (peer dependency)
|
|
31
|
+
- An RSA key pair for RS256 signing and verification
|
|
32
|
+
- A database adapter for services that register users, issue tokens, or use
|
|
33
|
+
the authentication router
|
|
28
34
|
|
|
29
|
-
`argon2` is a native dependency. Most
|
|
30
|
-
|
|
35
|
+
`argon2` is a native dependency. Most platforms use a prebuilt binary; unusual
|
|
36
|
+
platforms may require standard Node.js native build tools.
|
|
31
37
|
|
|
32
38
|
## Installation
|
|
33
39
|
|
|
@@ -35,26 +41,40 @@ unusual platforms may need standard Node.js native build tools.
|
|
|
35
41
|
npm install @nabhiraj/nab-auth express
|
|
36
42
|
```
|
|
37
43
|
|
|
38
|
-
## Quick start
|
|
44
|
+
## Quick start: MongoDB
|
|
45
|
+
|
|
46
|
+
The application loads its own environment configuration and passes the values
|
|
47
|
+
to the adapter. The library does not require a particular `.env` package or
|
|
48
|
+
variable naming convention.
|
|
39
49
|
|
|
40
50
|
```js
|
|
51
|
+
import "dotenv/config";
|
|
41
52
|
import express from "express";
|
|
42
|
-
import {
|
|
53
|
+
import {
|
|
54
|
+
createAuthenticationLibrary,
|
|
55
|
+
createMongoAuthStore
|
|
56
|
+
} from "@nabhiraj/nab-auth";
|
|
43
57
|
|
|
44
58
|
const app = express();
|
|
45
59
|
app.use(express.json());
|
|
46
60
|
|
|
47
61
|
const auth = createAuthenticationLibrary({
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
database: {
|
|
63
|
+
adapter: createMongoAuthStore,
|
|
64
|
+
adapterConfig: {
|
|
65
|
+
uri: process.env.MONGODB_URI,
|
|
66
|
+
databaseName: process.env.MONGODB_DB_NAME,
|
|
67
|
+
usersCollectionName: process.env.MONGODB_USERS_COLLECTION || "users",
|
|
68
|
+
potentialUsersCollectionName:
|
|
69
|
+
process.env.MONGODB_POTENTIAL_USERS_COLLECTION || "potential_users"
|
|
70
|
+
}
|
|
51
71
|
},
|
|
52
72
|
jwt: {
|
|
53
|
-
issuer:
|
|
54
|
-
audience:
|
|
73
|
+
issuer: process.env.JWT_ISSUER,
|
|
74
|
+
audience: process.env.JWT_AUDIENCE,
|
|
55
75
|
keys: {
|
|
56
|
-
privateKeyPath: process.env.
|
|
57
|
-
publicKeyPath: process.env.
|
|
76
|
+
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH,
|
|
77
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
58
78
|
}
|
|
59
79
|
},
|
|
60
80
|
defaultAdmin: { enabled: false }
|
|
@@ -69,10 +89,10 @@ app.use(
|
|
|
69
89
|
"/auth",
|
|
70
90
|
auth.createAuthRouter(createTokenPayload, {
|
|
71
91
|
sendRegistrationCode: async ({ email, verificationCode }) => {
|
|
72
|
-
await
|
|
92
|
+
await emailProvider.send(email, `Your verification code is ${verificationCode}`);
|
|
73
93
|
},
|
|
74
94
|
sendTwoFactorCode: async ({ email, verificationCode }) => {
|
|
75
|
-
await
|
|
95
|
+
await emailProvider.send(email, `Your 2FA code is ${verificationCode}`);
|
|
76
96
|
}
|
|
77
97
|
})
|
|
78
98
|
);
|
|
@@ -86,49 +106,139 @@ await auth.connectToDatabase();
|
|
|
86
106
|
app.listen(3000);
|
|
87
107
|
```
|
|
88
108
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
enabled.
|
|
109
|
+
The order matters: call `connectToDatabase()` before receiving requests, and
|
|
110
|
+
mount `jwtErrorHandler` after routes that use `requireJwt`.
|
|
92
111
|
|
|
93
112
|
## Core API
|
|
94
113
|
|
|
95
114
|
```js
|
|
96
115
|
import {
|
|
116
|
+
assertAuthStore,
|
|
117
|
+
authStoreMethods,
|
|
97
118
|
createAuthenticationLibrary,
|
|
98
119
|
createFileJwtKeyProvider,
|
|
120
|
+
createMongoAuthStore,
|
|
99
121
|
defaultAuthConfig
|
|
100
122
|
} from "@nabhiraj/nab-auth";
|
|
101
123
|
```
|
|
102
124
|
|
|
103
|
-
`createAuthenticationLibrary(config)` creates isolated state
|
|
104
|
-
|
|
125
|
+
`createAuthenticationLibrary(config)` creates isolated state. Its adapter,
|
|
126
|
+
JWT-key cache, middleware, and router are not shared with another library
|
|
105
127
|
instance.
|
|
106
128
|
|
|
107
129
|
| Member | Purpose |
|
|
108
130
|
| --- | --- |
|
|
109
|
-
| `connectToDatabase()` |
|
|
110
|
-
| `closeDatabase()` | Closes
|
|
111
|
-
| `createAuthRouter(payloadFactory, options)` | Creates the Express authentication router. |
|
|
112
|
-
| `requireJwt` |
|
|
113
|
-
| `requireNabAdmin` | Requires the configured administrator role
|
|
114
|
-
| `jwtErrorHandler` |
|
|
115
|
-
| `config` | The merged configuration
|
|
131
|
+
| `connectToDatabase()` | Creates the configured adapter, connects it, and creates the default admin when enabled. |
|
|
132
|
+
| `closeDatabase()` | Closes the adapter if it was created. |
|
|
133
|
+
| `createAuthRouter(payloadFactory, options)` | Creates the storage-backed Express authentication router. |
|
|
134
|
+
| `requireJwt` | Validates an access token and sets claims on `req.auth`. |
|
|
135
|
+
| `requireNabAdmin` | Requires the configured administrator role after `requireJwt`. |
|
|
136
|
+
| `jwtErrorHandler` | Converts `express-jwt` authentication failures to a `401` response. |
|
|
137
|
+
| `config` | The merged configuration for this library instance. |
|
|
138
|
+
|
|
139
|
+
`payloadFactory(user)` must return an object. Its properties are included in
|
|
140
|
+
new access tokens; the library adds `sub`, JWT metadata, and
|
|
141
|
+
`tokenType: "access"`.
|
|
142
|
+
|
|
143
|
+
## Database adapters
|
|
144
|
+
|
|
145
|
+
There is no default database. A storage-backed service must configure
|
|
146
|
+
`database.adapter` as a factory function and pass adapter-specific values in
|
|
147
|
+
`database.adapterConfig`.
|
|
148
|
+
|
|
149
|
+
```js
|
|
150
|
+
const auth = createAuthenticationLibrary({
|
|
151
|
+
database: {
|
|
152
|
+
adapter: createPostgresAuthStore,
|
|
153
|
+
adapterConfig: {
|
|
154
|
+
connectionString: process.env.DATABASE_URL,
|
|
155
|
+
usersTableName: "users",
|
|
156
|
+
pendingUsersTableName: "pending_users"
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
Import the adapter in the application and pass the function itself. Do not pass
|
|
163
|
+
a source-file path or make the library dynamically load a module.
|
|
164
|
+
|
|
165
|
+
### MongoDB adapter
|
|
166
|
+
|
|
167
|
+
The package exports `createMongoAuthStore`. Its adapter configuration is:
|
|
168
|
+
|
|
169
|
+
| Field | Required | Description |
|
|
170
|
+
| --- | --- | --- |
|
|
171
|
+
| `uri` | Yes | MongoDB or MongoDB Atlas connection URI. |
|
|
172
|
+
| `databaseName` | Yes | Database used exclusively for auth data. |
|
|
173
|
+
| `usersCollectionName` | Yes | Collection for permanent user records. |
|
|
174
|
+
| `potentialUsersCollectionName` | Yes | Collection for pending registration records. |
|
|
175
|
+
|
|
176
|
+
On `connect()`, the Mongo adapter creates missing collections and indexes:
|
|
177
|
+
|
|
178
|
+
- Unique indexes for `users.email` and `users.user_id`
|
|
179
|
+
- A unique index for pending-user `email`
|
|
180
|
+
- A TTL index on pending-user `expiresAt`
|
|
181
|
+
|
|
182
|
+
It also migrates existing Mongo user records that lack `user_id` or `nab_role`.
|
|
183
|
+
|
|
184
|
+
### Custom adapter contract
|
|
185
|
+
|
|
186
|
+
An adapter factory receives `adapterConfig` and returns an object that
|
|
187
|
+
implements every method below. `assertAuthStore(store)` validates the shape,
|
|
188
|
+
and `authStoreMethods` exports the method names.
|
|
189
|
+
|
|
190
|
+
| Method | Required behavior |
|
|
191
|
+
| --- | --- |
|
|
192
|
+
| `connect()` | Open the database connection and prepare storage. |
|
|
193
|
+
| `close()` | Release the database connection/resources. |
|
|
194
|
+
| `findUserByEmail(email)` | Return the user record or `null`. |
|
|
195
|
+
| `findUserByUserId(userId)` | Return the user record or `null`. |
|
|
196
|
+
| `createUser(user)` | Persist the user and return `true`; return `false` when its email already exists. |
|
|
197
|
+
| `upsertPotentialUser(email, hash, generatedAt, expiresAt)` | Create/update the pending registration; return `false` when the email is already registered. |
|
|
198
|
+
| `findPotentialUserByEmail(email)` | Return the pending record or `null`. |
|
|
199
|
+
| `deletePotentialUserByEmail(email)` | Delete the pending registration if present. |
|
|
200
|
+
| `updateUserPassword(userId, hash)` | Update the password hash; return whether a user matched. |
|
|
201
|
+
| `updateUserTwoFactorCode(userId, hash, generatedAt, expiresAt)` | Store a new 2FA code hash; return whether a user matched. |
|
|
202
|
+
| `consumeUserTwoFactorCode(userId, hash)` | Atomically consume exactly that unused 2FA hash; return whether it was consumed. |
|
|
203
|
+
| `addUserRoles(userId, roles)` | Add unique roles and return the updated user or `null`. |
|
|
204
|
+
| `removeUserRole(userId, role)` | Remove a role and return the updated user or `null`. |
|
|
205
|
+
| `replaceUserRoles(userId, roles)` | Replace all roles and return the updated user or `null`. |
|
|
206
|
+
|
|
207
|
+
User records returned by an adapter must provide at least:
|
|
116
208
|
|
|
117
|
-
|
|
118
|
-
|
|
209
|
+
```js
|
|
210
|
+
{
|
|
211
|
+
user_id: "uuid",
|
|
212
|
+
email: "person@example.com",
|
|
213
|
+
passwordHash: "argon2 hash",
|
|
214
|
+
nab_role: ["role"],
|
|
215
|
+
createdAt: new Date()
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
Pending records must provide `verificationCodeHash` and `codeExpiresAt` as a
|
|
220
|
+
JavaScript `Date`. User records with a pending 2FA challenge use
|
|
221
|
+
`twoFactorCodeHash` and `twoFactorCodeExpiresAt`.
|
|
222
|
+
|
|
223
|
+
The core normalizes email before it invokes the store. Custom adapters should
|
|
224
|
+
preserve that normalized email value and enforce unique email storage.
|
|
119
225
|
|
|
120
226
|
## Configuration
|
|
121
227
|
|
|
122
|
-
|
|
123
|
-
`
|
|
228
|
+
All settings use `defaultAuthConfig` when omitted. `database.adapter` is
|
|
229
|
+
required before calling `connectToDatabase()` or `createAuthRouter()`. A
|
|
230
|
+
verifier-only service can omit it.
|
|
124
231
|
|
|
125
232
|
```js
|
|
126
233
|
const auth = createAuthenticationLibrary({
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
234
|
+
database: {
|
|
235
|
+
adapter: createMongoAuthStore,
|
|
236
|
+
adapterConfig: {
|
|
237
|
+
uri: process.env.MONGODB_URI,
|
|
238
|
+
databaseName: process.env.MONGODB_DB_NAME,
|
|
239
|
+
usersCollectionName: "users",
|
|
240
|
+
potentialUsersCollectionName: "potential_users"
|
|
241
|
+
}
|
|
132
242
|
},
|
|
133
243
|
jwt: {
|
|
134
244
|
issuer: "my-service",
|
|
@@ -138,8 +248,8 @@ const auth = createAuthenticationLibrary({
|
|
|
138
248
|
refreshTokenTtl: "7d",
|
|
139
249
|
keyCacheTtlMs: 15 * 60 * 1000,
|
|
140
250
|
keys: {
|
|
141
|
-
privateKeyPath: process.env.
|
|
142
|
-
publicKeyPath: process.env.
|
|
251
|
+
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH,
|
|
252
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
143
253
|
}
|
|
144
254
|
},
|
|
145
255
|
authorization: {
|
|
@@ -158,11 +268,6 @@ const auth = createAuthenticationLibrary({
|
|
|
158
268
|
codeDigits: 6,
|
|
159
269
|
codeTtlMs: 10 * 60 * 1000
|
|
160
270
|
},
|
|
161
|
-
passwordHash: {
|
|
162
|
-
memoryCost: 19456,
|
|
163
|
-
timeCost: 2,
|
|
164
|
-
parallelism: 1
|
|
165
|
-
},
|
|
166
271
|
refreshCookie: {
|
|
167
272
|
name: "refresh_token",
|
|
168
273
|
httpOnly: true,
|
|
@@ -176,57 +281,70 @@ const auth = createAuthenticationLibrary({
|
|
|
176
281
|
},
|
|
177
282
|
defaultAdmin: {
|
|
178
283
|
enabled: false,
|
|
179
|
-
email: "admin@
|
|
180
|
-
password: "
|
|
284
|
+
email: "admin@example.com",
|
|
285
|
+
password: "use-a-strong-secret",
|
|
181
286
|
roles: ["nab_admin"]
|
|
182
287
|
}
|
|
183
288
|
});
|
|
184
289
|
```
|
|
185
290
|
|
|
186
|
-
|
|
187
|
-
to `
|
|
188
|
-
`false`;
|
|
291
|
+
`refreshCookie.secure` defaults to `false` for local HTTP development; set it
|
|
292
|
+
to `true` for HTTPS production deployments. `defaultAdmin.enabled` defaults to
|
|
293
|
+
`false`; do not enable it with a known or weak password.
|
|
294
|
+
|
|
295
|
+
### Registration and authorization policy
|
|
296
|
+
|
|
297
|
+
You can provide an optional password validator for registration:
|
|
298
|
+
|
|
299
|
+
```js
|
|
300
|
+
registration: {
|
|
301
|
+
validatePassword: (password) => password.length >= 12
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
The function receives the plaintext password and must return `true` or `false`;
|
|
306
|
+
async functions returning a boolean are also supported. A rejected password
|
|
307
|
+
receives HTTP `400` and is not stored. If omitted, the existing non-empty
|
|
308
|
+
password requirement applies. This setting applies to registration only, not
|
|
309
|
+
password changes or default-admin creation. Validator errors are passed to
|
|
310
|
+
Express error handling; the function must not log or persist passwords.
|
|
311
|
+
|
|
312
|
+
| Setting | Effect |
|
|
313
|
+
| --- | --- |
|
|
314
|
+
| `registration.verificationRequired` | Requires a valid registration code on `POST /register`. |
|
|
315
|
+
| `twoFactor.required` | Requires a valid 2FA code before issuing tokens. |
|
|
316
|
+
| `authorization.userRegistrationRequiresNabAdmin` | Requires a JWT-bearing administrator to register users. |
|
|
317
|
+
| `authorization.passwordChangeRequiresNabAdmin` | Requires a JWT-bearing administrator for all password changes. |
|
|
318
|
+
| `authorization.nabAdminCanChangeOtherUsersPasswords` | Lets an administrator change another user’s password when admin-only password changes are disabled. |
|
|
189
319
|
|
|
190
320
|
### Code delivery callbacks
|
|
191
321
|
|
|
322
|
+
Supply callbacks only if the associated feature is enabled:
|
|
323
|
+
|
|
192
324
|
```js
|
|
193
325
|
auth.createAuthRouter(createTokenPayload, {
|
|
194
|
-
sendRegistrationCode: async ({
|
|
195
|
-
email,
|
|
196
|
-
verificationCode,
|
|
197
|
-
codeGeneratedAt,
|
|
198
|
-
codeExpiresAt
|
|
199
|
-
}) => {
|
|
326
|
+
sendRegistrationCode: async ({ email, verificationCode }) => {
|
|
200
327
|
await emailProvider.send(email, verificationCode);
|
|
201
328
|
},
|
|
202
|
-
sendTwoFactorCode: async ({
|
|
203
|
-
email,
|
|
204
|
-
verificationCode,
|
|
205
|
-
codeGeneratedAt,
|
|
206
|
-
codeExpiresAt
|
|
207
|
-
}) => {
|
|
329
|
+
sendTwoFactorCode: async ({ email, verificationCode }) => {
|
|
208
330
|
await smsProvider.send(email, verificationCode);
|
|
209
331
|
}
|
|
210
332
|
});
|
|
211
333
|
```
|
|
212
334
|
|
|
213
|
-
`codeDelivery.allowCodeInResponseWithoutSender` controls missing
|
|
335
|
+
`codeDelivery.allowCodeInResponseWithoutSender` controls a missing callback:
|
|
214
336
|
|
|
215
|
-
- `true
|
|
337
|
+
- `true`: returns the generated code in the HTTP response. Use only for local
|
|
216
338
|
development or automated tests.
|
|
217
|
-
- `false
|
|
218
|
-
|
|
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.
|
|
339
|
+
- `false`: the route fails when a required callback is absent. Use this in
|
|
340
|
+
production.
|
|
223
341
|
|
|
224
|
-
## JWT
|
|
342
|
+
## JWT keys and rotation
|
|
225
343
|
|
|
226
|
-
The built-in key provider loads JSON files lazily and
|
|
344
|
+
The built-in file key provider loads JSON files lazily and caches them for
|
|
227
345
|
`jwt.keyCacheTtlMs`.
|
|
228
346
|
|
|
229
|
-
Private-key JSON
|
|
347
|
+
Private-key JSON contains the active signing key:
|
|
230
348
|
|
|
231
349
|
```json
|
|
232
350
|
{
|
|
@@ -235,7 +353,7 @@ Private-key JSON has one active signing key:
|
|
|
235
353
|
}
|
|
236
354
|
```
|
|
237
355
|
|
|
238
|
-
Public-key JSON
|
|
356
|
+
Public-key JSON contains every currently valid verification key:
|
|
239
357
|
|
|
240
358
|
```json
|
|
241
359
|
{
|
|
@@ -251,84 +369,99 @@ Public-key JSON keeps each currently valid verification key:
|
|
|
251
369
|
Rotation process:
|
|
252
370
|
|
|
253
371
|
1. Add the new public key and `kid` to the public-key JSON.
|
|
254
|
-
2. Switch the
|
|
255
|
-
3. Keep the
|
|
256
|
-
|
|
257
|
-
Never commit or publish private keys.
|
|
372
|
+
2. Switch the private-key JSON to the matching active key and `kid`.
|
|
373
|
+
3. Keep the old public key until tokens signed by it have expired.
|
|
258
374
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
Use a key provider for KMS, Vault, or a secret manager:
|
|
375
|
+
Never commit private keys. For KMS, Vault, or another secret manager, provide
|
|
376
|
+
`jwt.keyProvider` instead:
|
|
262
377
|
|
|
263
378
|
```js
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
return secretManager.getPublicKey(keyId);
|
|
275
|
-
}
|
|
379
|
+
jwt: {
|
|
380
|
+
keyProvider: {
|
|
381
|
+
async getActiveSigningKey() {
|
|
382
|
+
return {
|
|
383
|
+
keyId: "101",
|
|
384
|
+
privateKey: await secretManager.getActivePrivateKey()
|
|
385
|
+
};
|
|
386
|
+
},
|
|
387
|
+
async getPublicKey(keyId) {
|
|
388
|
+
return secretManager.getPublicKey(keyId);
|
|
276
389
|
}
|
|
277
390
|
}
|
|
278
|
-
}
|
|
391
|
+
}
|
|
279
392
|
```
|
|
280
393
|
|
|
281
|
-
When `jwt.keyProvider` is
|
|
394
|
+
When `jwt.keyProvider` is present, `jwt.keys` is not used.
|
|
282
395
|
|
|
283
396
|
## HTTP API
|
|
284
397
|
|
|
285
|
-
|
|
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
|
|
398
|
+
### Per-route middleware
|
|
304
399
|
|
|
305
|
-
|
|
400
|
+
Pass ordered arrays of Express middleware in `createAuthRouter` options:
|
|
306
401
|
|
|
307
|
-
```
|
|
308
|
-
{
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
402
|
+
```js
|
|
403
|
+
const router = auth.createAuthRouter(createTokenPayload, {
|
|
404
|
+
middleware: {
|
|
405
|
+
token: { beforeAuth: [loginLimiter, loginAudit] },
|
|
406
|
+
password: {
|
|
407
|
+
beforeAuth: [passwordIpLimiter],
|
|
408
|
+
afterAuth: [passwordUserLimiter]
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
app.use("/auth", router);
|
|
315
413
|
```
|
|
316
414
|
|
|
317
|
-
|
|
318
|
-
|
|
415
|
+
Supported keys are `potentialUsers`, `register`, `token`, `refresh`,
|
|
416
|
+
`twoFactorCode`, `password`, `addRoles`, `removeRole`, and `replaceRoles`.
|
|
417
|
+
Each omitted array defaults to empty. Middleware runs in array order:
|
|
418
|
+
`beforeAuth`, built-in JWT and role checks, `afterAuth`, then the controller.
|
|
419
|
+
`afterAuth` is only permitted for routes with built-in authentication:
|
|
420
|
+
password and role routes, plus registration when
|
|
421
|
+
`userRegistrationRequiresNabAdmin` is enabled. The authenticated claims are
|
|
422
|
+
available as `req.auth` after those checks. Middleware may end the response or
|
|
423
|
+
call `next(error)` in the usual Express way.
|
|
424
|
+
|
|
425
|
+
The application supplies and configures limiters, including any shared store
|
|
426
|
+
needed across processes. Apply middleware to application-owned routes directly
|
|
427
|
+
with Express. Configure Express proxy trust to match your deployment before
|
|
428
|
+
using client IP addresses for rate limiting.
|
|
429
|
+
|
|
430
|
+
Mount the router at `/auth` to use these routes:
|
|
431
|
+
|
|
432
|
+
| Method and route | Request body | Success | Notes |
|
|
433
|
+
| --- | --- | --- | --- |
|
|
434
|
+
| `POST /auth/potential-users` | `email` | `201` or `202` | Starts/refreshes registration verification. `202` is used when a sender callback is configured. |
|
|
435
|
+
| `POST /auth/register` | `email`, `password`, optional `verificationCode` | `201` | Code required when registration verification is enabled. |
|
|
436
|
+
| `POST /auth/token` | `email`, `password`, optional `twoFactorCode` | `200` | Returns access token and sets refresh-token cookie. |
|
|
437
|
+
| `POST /auth/refresh` | Optional `refresh_token` | `200` | Uses body token first, then the configured cookie. Issues a new refresh token. |
|
|
438
|
+
| `POST /auth/2fa/code` | `email`, `password` | `201` or `202` | Only useful when 2FA is enabled. |
|
|
439
|
+
| `PATCH /auth/password` | `newPassword`, optional `user_id` or `email` | `204` | Requires JWT; authorization settings can require admin access. |
|
|
440
|
+
| `POST /auth/roles` | `roles`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
441
|
+
| `DELETE /auth/roles` | `role`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
442
|
+
| `PUT /auth/roles` | `roles`, exactly one of `user_id` or `email` | `200` | Requires administrator access token. |
|
|
443
|
+
|
|
444
|
+
Access tokens include the application payload. Refresh tokens contain only the
|
|
445
|
+
subject and token metadata. The library does not persist refresh tokens or
|
|
446
|
+
revoke previously issued refresh tokens; the host application should add a
|
|
447
|
+
token-revocation strategy if that is required.
|
|
319
448
|
|
|
320
449
|
## Verifier-only service
|
|
321
450
|
|
|
322
|
-
A
|
|
451
|
+
A service that only validates access tokens needs no database adapter and no
|
|
323
452
|
private key:
|
|
324
453
|
|
|
325
454
|
```js
|
|
455
|
+
import express from "express";
|
|
456
|
+
import { createAuthenticationLibrary } from "@nabhiraj/nab-auth";
|
|
457
|
+
|
|
458
|
+
const app = express();
|
|
326
459
|
const auth = createAuthenticationLibrary({
|
|
327
460
|
jwt: {
|
|
328
461
|
issuer: "my-service",
|
|
329
462
|
audience: "my-client",
|
|
330
463
|
keys: {
|
|
331
|
-
publicKeyPath: process.env.
|
|
464
|
+
publicKeyPath: process.env.JWT_PUBLIC_KEY_PATH
|
|
332
465
|
}
|
|
333
466
|
}
|
|
334
467
|
});
|
|
@@ -340,16 +473,39 @@ app.get("/api/business-data", auth.requireJwt, (req, res) => {
|
|
|
340
473
|
app.use(auth.jwtErrorHandler);
|
|
341
474
|
```
|
|
342
475
|
|
|
476
|
+
Do not call `connectToDatabase()` or `createAuthRouter()` in this mode.
|
|
477
|
+
|
|
478
|
+
## Testing
|
|
479
|
+
|
|
480
|
+
Run unit and adapter-contract tests:
|
|
481
|
+
|
|
482
|
+
```bash
|
|
483
|
+
npm test
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
Run the opt-in live MongoDB end-to-end suite only with dedicated test
|
|
487
|
+
credentials and an isolated test database:
|
|
488
|
+
|
|
489
|
+
```bash
|
|
490
|
+
npm run test:e2e:mongo
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
See [E2E_TEST_PLAN.md](E2E_TEST_PLAN.md) for required endpoint and
|
|
494
|
+
configuration coverage. Never point live tests at the host application’s
|
|
495
|
+
database.
|
|
496
|
+
|
|
343
497
|
## Production checklist
|
|
344
498
|
|
|
345
|
-
-
|
|
346
|
-
-
|
|
347
|
-
-
|
|
348
|
-
-
|
|
349
|
-
- Add application-level rate limits to login, registration, code
|
|
350
|
-
and password-change routes.
|
|
351
|
-
- Configure CORS and
|
|
352
|
-
- Retain old public keys until
|
|
499
|
+
- Keep database credentials and private keys outside source control.
|
|
500
|
+
- Use HTTPS and `refreshCookie.secure: true`.
|
|
501
|
+
- Set `codeDelivery.allowCodeInResponseWithoutSender: false`.
|
|
502
|
+
- Provide real code-delivery callbacks for enabled verification features.
|
|
503
|
+
- Add application-level rate limits to login, registration, code generation,
|
|
504
|
+
refresh, and password-change routes.
|
|
505
|
+
- Configure CORS and `sameSite` for the frontend topology.
|
|
506
|
+
- Retain old public keys until every token signed with them expires.
|
|
507
|
+
- Define refresh-token revocation if logout, breach response, or session
|
|
508
|
+
invalidation requires it.
|
|
353
509
|
|
|
354
510
|
## Support and contributing
|
|
355
511
|
|
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.1.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,18 @@ export function createAuthConfig(overrides = {}) {
|
|
|
15
15
|
validateCodeConfig(config.twoFactor, "twoFactor", "required");
|
|
16
16
|
validateString(config.authorization.adminRole, "authorization.adminRole");
|
|
17
17
|
|
|
18
|
+
if (config.registration.validatePassword !== undefined &&
|
|
19
|
+
typeof config.registration.validatePassword !== "function") {
|
|
20
|
+
throw new TypeError("registration.validatePassword must be a function");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (
|
|
24
|
+
config.database.adapter !== undefined &&
|
|
25
|
+
typeof config.database.adapter !== "function"
|
|
26
|
+
) {
|
|
27
|
+
throw new TypeError("database.adapter must be an adapter factory function");
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
if (typeof config.codeDelivery.allowCodeInResponseWithoutSender !== "boolean") {
|
|
19
31
|
throw new TypeError(
|
|
20
32
|
"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",
|
|
@@ -26,6 +24,7 @@ export const defaultAuthConfig = {
|
|
|
26
24
|
},
|
|
27
25
|
registration: {
|
|
28
26
|
verificationRequired: false,
|
|
27
|
+
validatePassword: undefined,
|
|
29
28
|
codeDigits: 6,
|
|
30
29
|
codeTtlMs: 10 * 60 * 1000
|
|
31
30
|
},
|
|
@@ -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,
|
|
@@ -142,6 +143,16 @@ export function createAuthController({
|
|
|
142
143
|
return res.status(400).json({ message: "email and password are required" });
|
|
143
144
|
}
|
|
144
145
|
|
|
146
|
+
if (config.registration.validatePassword) {
|
|
147
|
+
const valid = await config.registration.validatePassword(password);
|
|
148
|
+
if (typeof valid !== "boolean") {
|
|
149
|
+
throw new TypeError("registration.validatePassword must return a boolean");
|
|
150
|
+
}
|
|
151
|
+
if (!valid) {
|
|
152
|
+
return res.status(400).json({ message: "Password does not meet requirements" });
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
145
156
|
const normalizedEmail = normalizeEmail(email);
|
|
146
157
|
if (await repository.findUserByEmail(normalizedEmail)) {
|
|
147
158
|
return res.status(409).json({ message: "User already exists" });
|
|
@@ -170,23 +181,24 @@ export function createAuthController({
|
|
|
170
181
|
}
|
|
171
182
|
}
|
|
172
183
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
)
|
|
184
|
+
const userId = randomUUID();
|
|
185
|
+
const userCreated = await repository.createUser({
|
|
186
|
+
user_id: userId,
|
|
187
|
+
email: normalizedEmail,
|
|
188
|
+
passwordHash: await hashPassword(password, config.passwordHash),
|
|
189
|
+
nab_role: [],
|
|
190
|
+
createdAt: new Date()
|
|
191
|
+
});
|
|
178
192
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
if (!userCreated) {
|
|
194
|
+
return res.status(409).json({ message: "User already exists" });
|
|
195
|
+
}
|
|
182
196
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
if (error.code === 11000) {
|
|
186
|
-
return res.status(409).json({ message: "User already exists" });
|
|
187
|
-
}
|
|
188
|
-
throw error;
|
|
197
|
+
if (config.registration.verificationRequired) {
|
|
198
|
+
await repository.deletePotentialUserByEmail(normalizedEmail);
|
|
189
199
|
}
|
|
200
|
+
|
|
201
|
+
return res.status(201).json({ user_id: userId, email: normalizedEmail });
|
|
190
202
|
} catch (error) {
|
|
191
203
|
next(error);
|
|
192
204
|
}
|
|
@@ -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,21 +37,33 @@ 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
|
|
38
64
|
});
|
|
39
65
|
|
|
40
|
-
return createAuthRouter({ controller, middleware, config });
|
|
66
|
+
return createAuthRouter({ controller, middleware, config, routeMiddleware: options.middleware });
|
|
41
67
|
}
|
|
42
68
|
};
|
|
43
69
|
}
|
|
@@ -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";
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Router } from "express";
|
|
2
2
|
|
|
3
|
-
export function createAuthRouter({ controller, middleware, config }) {
|
|
3
|
+
export function createAuthRouter({ controller, middleware, config, routeMiddleware = {} }) {
|
|
4
4
|
const authRouter = Router();
|
|
5
|
+
console.log('route middleware is ',routeMiddleware);
|
|
5
6
|
|
|
6
7
|
const registrationMiddleware = config.authorization.userRegistrationRequiresNabAdmin
|
|
7
8
|
? [middleware.requireJwt, middleware.requireNabAdmin]
|
|
@@ -14,27 +15,49 @@ export function createAuthRouter({ controller, middleware, config }) {
|
|
|
14
15
|
middleware.requireNabAdmin
|
|
15
16
|
];
|
|
16
17
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
"/roles",
|
|
20
|
-
|
|
21
|
-
controller.
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"/
|
|
25
|
-
|
|
26
|
-
controller.
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
18
|
+
const routes = {
|
|
19
|
+
addRoles: ["post", "/roles", roleManagementMiddleware, controller.addRoles],
|
|
20
|
+
removeRole: ["delete", "/roles", roleManagementMiddleware, controller.removeRole],
|
|
21
|
+
replaceRoles: ["put", "/roles", roleManagementMiddleware, controller.replaceRoles],
|
|
22
|
+
password: ["patch", "/password", passwordChangeMiddleware, controller.changePassword],
|
|
23
|
+
potentialUsers: ["post", "/potential-users", [], controller.createPotentialUser],
|
|
24
|
+
twoFactorCode: ["post", "/2fa/code", [], controller.generateTwoFactorCode],
|
|
25
|
+
register: ["post", "/register", registrationMiddleware, controller.registerUser],
|
|
26
|
+
token: ["post", "/token", [], controller.issueToken],
|
|
27
|
+
refresh: ["post", "/refresh", [], controller.refreshToken]
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
if (!routeMiddleware || typeof routeMiddleware !== "object" || Array.isArray(routeMiddleware)) {
|
|
31
|
+
throw new TypeError("options.middleware must be an object");
|
|
32
|
+
}
|
|
33
|
+
for (const name of Object.keys(routeMiddleware)) {
|
|
34
|
+
if (!Object.hasOwn(routes, name)) {
|
|
35
|
+
throw new TypeError(`Unknown middleware route: ${name}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
for (const [name, [method, path, auth, handler]] of Object.entries(routes)) {
|
|
40
|
+
const settings = routeMiddleware[name] ?? {};
|
|
41
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
42
|
+
throw new TypeError(`options.middleware.${name} must be an object`);
|
|
43
|
+
}
|
|
44
|
+
for (const key of Object.keys(settings)) {
|
|
45
|
+
if (key !== "beforeAuth" && key !== "afterAuth") {
|
|
46
|
+
throw new TypeError(`Unknown middleware position: ${name}.${key}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const before = settings.beforeAuth ?? [];
|
|
50
|
+
const after = settings.afterAuth ?? [];
|
|
51
|
+
for (const [position, value] of [["beforeAuth", before], ["afterAuth", after]]) {
|
|
52
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "function")) {
|
|
53
|
+
throw new TypeError(`options.middleware.${name}.${position} must be an array of functions`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (!auth.length && Object.hasOwn(settings, "afterAuth")) {
|
|
57
|
+
throw new TypeError(`options.middleware.${name}.afterAuth requires built-in authentication`);
|
|
58
|
+
}
|
|
59
|
+
authRouter[method](path, ...before, ...auth, ...after, handler);
|
|
60
|
+
}
|
|
38
61
|
|
|
39
62
|
return authRouter;
|
|
40
63
|
}
|
|
@@ -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
|
+
}
|