@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 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
- # Temporary Holding Version
1
+ # nab-auth
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ [![license](https://img.shields.io/npm/l/@nabhiraj/nab-auth.svg)](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.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
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
+ };