@medi-car-bd/mcb-shared-auth 0.0.13 → 0.0.15

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.
Files changed (3) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +277 -19
  3. package/package.json +77 -64
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2024 FatihOnlinEShopBD
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.
1
+ MIT License
2
+
3
+ Copyright (c) 2024 FatihOnlinEShopBD
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,44 +1,302 @@
1
- # mcb-backend-boilerplate
1
+ # @medi-car-bd/mcb-shared-auth
2
+
3
+ Shared authentication library for the Medi Car BD microservices platform.
4
+
5
+ One implementation of JWT issuance, refresh-token rotation and role-based
6
+ authorization, used by every backend service. When a service and the API
7
+ gateway disagree about what a valid token is, the disagreement becomes an
8
+ authentication bug — this package exists to make that impossible.
9
+
10
+ **It is a library, not a service.** It has no database, no HTTP server and no
11
+ ORM. You inject the persistence it needs through an interface, which is what
12
+ lets the auth service own a database while every other service owns none.
13
+
14
+ Currently at **v0.0.8**. Used in production across `mcb-auth-service`,
15
+ `mcb-api-gateway`, `mcb-hrm-client`, `mcb-org-client`, `mcb-billing-client` and
16
+ `mcb-dashboard-client`.
17
+
18
+ ## Install
2
19
 
3
20
  ```bash
4
- rm -r node_modules package-lock.json
21
+ pnpm add @medi-car-bd/mcb-shared-auth
22
+ # or
23
+ npm install @medi-car-bd/mcb-shared-auth
5
24
  ```
6
25
 
7
- - Prisma Generate
26
+ Requires **Express 4** as a peer dependency (`^4.18.2`). The package targets
27
+ CommonJS and ships type declarations.
8
28
 
9
- ```bash
10
- pnpm prisma generate
29
+ ## Quick start
30
+
31
+ ### Protect a route
32
+
33
+ ```ts
34
+ import {
35
+ authenticate,
36
+ authorize,
37
+ type AuthenticatedRequest,
38
+ } from "@medi-car-bd/mcb-shared-auth";
39
+
40
+ router.get("/employees", authenticate, authorize("HR_MANAGER"), handler);
41
+
42
+ const handler = (req: AuthenticatedRequest, res: Response) => {
43
+ const { sub: userId, role, tenantId } = req.user!;
44
+ // ...
45
+ };
11
46
  ```
12
47
 
13
- - Run Prisma To Initialize Database
48
+ `authenticate` accepts a `Bearer` token or an `x-access-token` header, and puts
49
+ the decoded claims on `req.user`. `authorize(...)` is a role guard that returns
50
+ `403` when the caller's `role` is not in the list. Called with no roles it
51
+ passes through, so it is safe to register unconditionally.
14
52
 
15
- ```bash
16
- pnpm prisma init
53
+ ### Issue a token pair
54
+
55
+ Only the credential authority should call this. Persistence is yours to
56
+ provide:
57
+
58
+ ```ts
59
+ import {
60
+ generateTokenPair,
61
+ type RefreshTokenRepository,
62
+ } from "@medi-car-bd/mcb-shared-auth";
63
+
64
+ const { accessToken, refreshToken } = await generateTokenPair({
65
+ user: { id, email, name, role, tenantId },
66
+ deviceInfo: req.headers["user-agent"],
67
+ ipAddress: req.ip,
68
+ config: {
69
+ jwtSecret: JWT_SECRET,
70
+ refreshTokenSecret: JWT_REFRESH_SECRET, // use a different secret
71
+ accessTokenExpiresIn: "15m",
72
+ refreshTokenExpiresIn: "7d",
73
+ },
74
+ repo: refreshTokenRepository, // your implementation
75
+ });
17
76
  ```
18
77
 
19
- - Generate Prisma Client
78
+ ### Rotate a refresh token
20
79
 
21
- ```bash
22
- pnpm prisma generate
80
+ ```ts
81
+ const result = await rotateRefreshToken({ oldRefreshToken, config, repo });
82
+ ```
83
+
84
+ Rotation is single-use. Each refresh issues a new pair and marks the old token
85
+ `replacedBy`. Presenting an already-rotated token is treated as theft and
86
+ rejected, which is how a stolen refresh token gets surfaced instead of quietly
87
+ reused.
88
+
89
+ ### Revoke everything for a user
23
90
 
91
+ ```ts
92
+ await revokeAllUserTokens({ userId, reason: "password_change", repo });
24
93
  ```
25
94
 
26
- - Run Prisma Migrations
95
+ Use this on password change and on admin-initiated lockouts. Pass
96
+ `excludeTokenId` to keep the current session alive.
27
97
 
28
- ```bash
29
- pnpm prisma:migrate
98
+ ## What is implemented
99
+
100
+ Every export below is available from the package root.
101
+
102
+ ### Tokens
103
+
104
+ | Export | Purpose |
105
+ | ---------------------- | ----------------------------------------------------------------------- |
106
+ | `generateTokenPair` | Issue an access/refresh pair and persist the hashed refresh token |
107
+ | `generateAccessToken` | Sign an access token; includes `role` and `tenantId` when present |
108
+ | `generateRefreshToken` | Sign a refresh token, returning both the raw value and its SHA-256 hash |
109
+ | `rotateRefreshToken` | Single-use rotation with reuse detection |
110
+ | `revokeAllUserTokens` | Bulk revocation, optionally sparing the current session |
111
+ | `generateTokenId` | 32-byte random `jti` |
112
+ | `hashToken` | SHA-256 hex digest, for storing a token safely |
113
+ | `signToken` | Minimal signer driven by env vars, for simple cases |
114
+
115
+ Access tokens are signed with `audience: "mcb-services"` and `issuer:
116
+ "mcb-auth"`. Refresh tokens support a separate signing secret via
117
+ `config.refreshTokenSecret`, falling back to the access-token secret.
118
+
119
+ ### Middleware
120
+
121
+ | Export | Purpose |
122
+ | ---------------- | ------------------------------------------------------- |
123
+ | `authenticate` | Verify a token and attach claims to `req.user` |
124
+ | `authorize` | Role guard: `authorize("ORG_ADMIN", "SUPER_ADMIN")` |
125
+ | `notFound` | `404` handler |
126
+ | `errorHandler` | Central error responder, normalising known error shapes |
127
+ | `processRequest` | Request logger that stamps the correlation id |
128
+
129
+ ### Utilities
130
+
131
+ | Export | Purpose |
132
+ | ----------------- | ----------------------------------------------------------- |
133
+ | `comparePassword` | bcryptjs password comparison |
134
+ | `limiter` | `express-rate-limit` instance, 100 requests / 15 min |
135
+ | `AppError` | Operational error carrying an HTTP status |
136
+ | `catchAsync` | Wrap an async handler so rejections reach the error handler |
137
+ | `logger` | Winston logger with daily rotating files |
138
+
139
+ ### Types
140
+
141
+ `AccessTokenPayload`, `RefreshTokenPayload`, `TokenConfig`, `TokenPair`,
142
+ `RefreshTokenRecord`, `RefreshTokenRepository`, `GenerateTokenPairParams`,
143
+ `RotateRefreshTokenParams`, `RotateRefreshTokenResult`,
144
+ `RevokeAllUserTokensParams`, `AuthenticatedRequest`, `TUser`.
145
+
146
+ ## The repository interface
147
+
148
+ The package never touches a database. It calls the `RefreshTokenRepository`
149
+ interface, and the consuming service implements it:
150
+
151
+ ```ts
152
+ interface RefreshTokenRepository {
153
+ create(record: RefreshTokenRecord): Promise<void>;
154
+ findById(id: string): Promise<RefreshTokenRecord | null>;
155
+ update(id: string, data: Partial<RefreshTokenRecord>): Promise<void>;
156
+ updateMany(where, data): Promise<{ count: number }>;
157
+ }
30
158
  ```
31
159
 
32
- - Shared Auth package installation
160
+ This is deliberate. A shared auth library that imported Prisma would force an
161
+ ORM on every consumer and would turn a security primitive into a data-access
162
+ layer. Keep it this way: implement the interface in the service that owns the
163
+ data, and let the other services stay stateless.
33
164
 
34
- ```bash
35
- npm login
165
+ `findById` **must** return the related `user` with `id`, `email`, `name`,
166
+ `role` and `tenantId`. Rotation needs those claims to mint the replacement
167
+ access token, and it assumes they are present.
168
+
169
+ ## Environment variables
170
+
171
+ Read at import time, so they must be set before the package is loaded.
172
+
173
+ | Variable | Used by | Notes |
174
+ | ---------------- | --------------------------- | ------------------------------- |
175
+ | `JWT_SECRET` | `authenticate`, `signToken` | **Required** |
176
+ | `JWT_EXPIRES_IN` | `signToken` | e.g. `15m` |
177
+ | `NODE_ENV` | `logger` | Selects console vs file logging |
178
+
179
+ Everything else — token lifetimes and the refresh secret — is passed explicitly
180
+ through `TokenConfig`, so there is no hidden global state.
181
+
182
+ ## Recommendations
183
+
184
+ Ordered by how much they matter.
185
+
186
+ ### 1. `authenticate` does not yet verify `audience` or `issuer`
36
187
 
188
+ Tokens are _issued_ with `aud` and `iss`, but `authenticate` verifies only the
189
+ signature and expiry. Because every service signs with one shared secret, a
190
+ token minted for any other purpose is currently accepted anywhere.
191
+
192
+ The fix is three lines inside `authenticate`, and it belongs here rather than in
193
+ each consumer so that all callers get it at once:
194
+
195
+ ```ts
196
+ jwt.verify(token, String(JWT_SECRET), {
197
+ audience: "mcb-services",
198
+ issuer: "mcb-auth",
199
+ });
200
+ ```
201
+
202
+ Tracked, not yet applied.
203
+
204
+ ### 2. Fail fast on a missing `JWT_SECRET`
205
+
206
+ `authenticate` calls `String(JWT_SECRET)`, which turns an unset variable into
207
+ the literal string `"undefined"` and silently accepts tokens signed with it.
208
+ Validate the secret at startup and exit instead:
209
+
210
+ ```ts
211
+ if (!JWT_SECRET) throw new Error("JWT_SECRET is not configured");
212
+ ```
213
+
214
+ ### 3. Use a separate refresh-token secret
215
+
216
+ Set `refreshTokenSecret` to a different value from `jwtSecret`. Otherwise
217
+ leaking the access-token signing key also lets an attacker mint refresh tokens
218
+ with any `sub`, which is a complete account takeover.
219
+
220
+ ### 4. Strip stack traces from error responses in production
221
+
222
+ `errorHandler` always includes `stack` in the JSON body. Gate it on `NODE_ENV`
223
+ before exposing the library publicly.
224
+
225
+ ### 5. Avoid `processRequest` on credential routes
226
+
227
+ `processRequest` serialises `req.body` into the log file. On a login or
228
+ register route that writes plaintext passwords to disk. Either drop body
229
+ logging or skip that middleware on those routes.
230
+
231
+ ### 6. Swap the in-memory limiter for Redis when you run more than one instance
232
+
233
+ `limiter` keeps counters in process memory, so with N replicas each one
234
+ effectively allows N times the configured rate. The message it returns also
235
+ promises "in an hour" while the window is 15 minutes.
236
+
237
+ ```ts
238
+ import { RedisStore } from "rate-limit-redis";
239
+ import { rateLimit } from "express-rate-limit";
240
+
241
+ rateLimit({
242
+ windowMs: 15 * 60 * 1000,
243
+ limit: 100,
244
+ store: new RedisStore({
245
+ prefix: "rl:",
246
+ sendCommand: (...a) => redis.call(...a),
247
+ }),
248
+ });
249
+ ```
250
+
251
+ Also set `app.set("trust proxy", ...)` behind a load balancer, or every caller
252
+ shares the proxy's address and per-IP limiting stops working.
253
+
254
+ ### 7. Prefer asynchronous password comparison
255
+
256
+ `comparePassword` uses `compareSync`, which blocks the event loop. That is
257
+ visible under login load. Switch to `bcryptjs.compare` when you need throughput.
258
+
259
+ ### 8. Short access-token lifetime plus rotation
260
+
261
+ `15m` access tokens with `7d` rotating refresh tokens keeps the blast radius of
262
+ a leaked access token small, and rotation makes reuse detectable. Long-lived
263
+ access tokens remove both properties.
264
+
265
+ ### Housekeeping
266
+
267
+ - `types/jwt.ts` still documents a `TokenUser` type that no longer exists
268
+ (removed in `0.0.8`); the shapes are inline.
269
+ - `TUser` and `TGenerateSKU` are boilerplate leftovers and unused by the
270
+ platform.
271
+ - `authorize` is typed `Array<string | number>` but only ever compares a string
272
+ `role`.
273
+
274
+ ## Publishing
275
+
276
+ ```bash
277
+ pnpm build # must run first: package.json "files" is ["dist"]
37
278
  npm version patch
279
+ npm publish --access public
280
+ ```
281
+
282
+ The package is scoped `@medi-car-bd` and `publishConfig.access` is already
283
+ `public`, so `--access public` is belt-and-braces.
284
+
285
+ To authenticate, use an automation token rather than a long-lived personal one:
38
286
 
287
+ ```bash
39
288
  npm config set //registry.npmjs.org/:_authToken=npm_xxxxxxxxxxxxxxxxx
289
+ ```
40
290
 
41
- npm publish --access public
291
+ ## Development
42
292
 
43
- pnpm install @mcb-shared-auth/auth
293
+ ```bash
294
+ pnpm install
295
+ pnpm typecheck
296
+ pnpm lint
297
+ pnpm build
44
298
  ```
299
+
300
+ ## License
301
+
302
+ MIT
package/package.json CHANGED
@@ -1,64 +1,77 @@
1
- {
2
- "name": "@medi-car-bd/mcb-shared-auth",
3
- "version": "0.0.13",
4
- "description": "Shared authentication module for MCB applications",
5
- "packageManager": "pnpm@10.18.0",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
8
- "publishConfig": {
9
- "access": "public"
10
- },
11
- "scripts": {
12
- "preinstall": "node -e \"\"",
13
- "postinstall": "",
14
- "build": "tsc",
15
- "start": "node dist/index.js",
16
- "typecheck": "tsc -p tsconfig.json --noEmit",
17
- "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
18
- "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
19
- "format": "prettier --write .",
20
- "format:check": "prettier --check ."
21
- },
22
- "keywords": [],
23
- "author": "MediCarBD",
24
- "license": "MIT",
25
- "devDependencies": {
26
- "@types/bcryptjs": "^2.4.6",
27
- "@types/express": "^4.17.21",
28
- "@types/jsonwebtoken": "^9.0.5",
29
- "@types/node": "^20.11.0",
30
- "@typescript-eslint/eslint-plugin": "^8.54.0",
31
- "@typescript-eslint/parser": "^8.54.0",
32
- "eslint": "^9.39.2",
33
- "eslint-config-prettier": "^9.1.0",
34
- "prettier": "^3.2.5",
35
- "typescript": "^5.3.3"
36
- },
37
- "dependencies": {
38
- "bcryptjs": "^2.4.3",
39
- "dotenv": "^16.3.1",
40
- "express-rate-limit": "^7.1.5",
41
- "jsonwebtoken": "^9.0.2",
42
- "winston": "^3.11.0",
43
- "winston-daily-rotate-file": "^4.7.1"
44
- },
45
- "peerDependencies": {
46
- "express": "^4.18.2"
47
- },
48
- "files": [
49
- "dist"
50
- ],
51
- "lint-staged": {
52
- "*.{ts,tsx,js,jsx}": [
53
- "eslint --max-warnings=0 --fix",
54
- "prettier --write"
55
- ],
56
- "*.{json,md,yml,yaml}": [
57
- "prettier --write"
58
- ]
59
- },
60
- "repository": {
61
- "type": "git",
62
- "url": "git+https://github.com/mcb-org/mcb-shared-auth.git"
63
- }
64
- }
1
+ {
2
+ "name": "@medi-car-bd/mcb-shared-auth",
3
+ "version": "0.0.15",
4
+ "description": "Shared authentication module for MCB applications",
5
+ "packageManager": "pnpm@10.18.0",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "scripts": {
12
+ "preinstall": "node -e \"\"",
13
+ "postinstall": "",
14
+ "build": "tsc",
15
+ "start": "node dist/index.js",
16
+ "typecheck": "tsc -p tsconfig.json --noEmit",
17
+ "lint": "eslint . --ext .ts,.tsx,.js,.jsx",
18
+ "lint:fix": "eslint . --ext .ts,.tsx,.js,.jsx --fix",
19
+ "format": "prettier --write .",
20
+ "format:check": "prettier --check ."
21
+ },
22
+ "keywords": [
23
+ "jwt",
24
+ "authentication",
25
+ "express",
26
+ "authorization",
27
+ "token-rotation"
28
+ ],
29
+ "author": {
30
+ "name": "Medi-Car-BD",
31
+ "url": "https://github.com/mcb-org"
32
+ },
33
+ "license": "MIT",
34
+ "devDependencies": {
35
+ "@types/bcryptjs": "^2.4.6",
36
+ "@types/express": "^4.17.21",
37
+ "@types/jsonwebtoken": "^9.0.5",
38
+ "@types/node": "^20.11.0",
39
+ "@typescript-eslint/eslint-plugin": "^8.54.0",
40
+ "@typescript-eslint/parser": "^8.54.0",
41
+ "eslint": "^9.39.2",
42
+ "eslint-config-prettier": "^9.1.0",
43
+ "prettier": "^3.2.5",
44
+ "typescript": "^5.3.3"
45
+ },
46
+ "dependencies": {
47
+ "bcryptjs": "^2.4.3",
48
+ "dotenv": "^16.3.1",
49
+ "express-rate-limit": "^7.1.5",
50
+ "jsonwebtoken": "^9.0.2",
51
+ "winston": "^3.11.0",
52
+ "winston-daily-rotate-file": "^4.7.1"
53
+ },
54
+ "peerDependencies": {
55
+ "express": "^4.18.2"
56
+ },
57
+ "files": [
58
+ "dist"
59
+ ],
60
+ "lint-staged": {
61
+ "*.{ts,tsx,js,jsx}": [
62
+ "eslint --max-warnings=0 --fix",
63
+ "prettier --write"
64
+ ],
65
+ "*.{json,md,yml,yaml}": [
66
+ "prettier --write"
67
+ ]
68
+ },
69
+ "repository": {
70
+ "type": "git",
71
+ "url": "git+https://github.com/mcb-org/mcb-shared-auth.git"
72
+ },
73
+ "homepage": "https://github.com/mcb-org/mcb-shared-auth#readme",
74
+ "bugs": {
75
+ "url": "https://github.com/mcb-org/mcb-shared-auth/issues"
76
+ }
77
+ }