@dwtechs/toker-express 0.7.1 โ 0.7.3
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 +21 -24
- package/dist/toker-express.js +64 -43
- package/package.json +18 -14
package/README.md
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
[](https://opensource.org/licenses/MIT)
|
|
3
3
|
[](https://www.npmjs.com/package/@dwtechs/toker-express)
|
|
4
4
|
[](https://www.npmjs.com/package/@dwtechs/toker-express)
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
- [Synopsis](#synopsis)
|
|
9
|
-
- [Support](#support)
|
|
10
9
|
- [Installation](#installation)
|
|
11
10
|
- [Usage](#usage)
|
|
12
11
|
- [Environment variables](#environment-variables)
|
|
13
12
|
- [API Reference](#api-reference)
|
|
14
13
|
- [Logs](#logs)
|
|
14
|
+
- [Support](#support)
|
|
15
15
|
- [Contributors](#contributors)
|
|
16
16
|
- [Stack](#stack)
|
|
17
17
|
|
|
@@ -23,17 +23,10 @@ It includes @dwtechs/toker library and adds Express middlewares to be used in a
|
|
|
23
23
|
|
|
24
24
|
- ๐ชถ Very lightweight
|
|
25
25
|
- ๐งช Thoroughly tested
|
|
26
|
-
- ๐ Shipped as
|
|
26
|
+
- ๐ Shipped as ES2022 ECMAScript module
|
|
27
27
|
- ๐ Written in Typescript
|
|
28
28
|
|
|
29
29
|
|
|
30
|
-
## Support
|
|
31
|
-
|
|
32
|
-
- node: 22
|
|
33
|
-
|
|
34
|
-
This is the oldest targeted versions.
|
|
35
|
-
|
|
36
|
-
|
|
37
30
|
## Installation
|
|
38
31
|
|
|
39
32
|
```bash
|
|
@@ -110,7 +103,7 @@ You can intialise the library using the following environment variables:
|
|
|
110
103
|
These environment variables will update the default values of the lib at start up.
|
|
111
104
|
So you do not need to init the library in the code.
|
|
112
105
|
|
|
113
|
-
Note that **TOKEN_SECRET** is mandatory.
|
|
106
|
+
Note that **TOKEN_SECRET** is mandatory and must be at least 32 characters long.
|
|
114
107
|
|
|
115
108
|
Default values :
|
|
116
109
|
|
|
@@ -187,13 +180,13 @@ function refreshTokens(req: Request, res: Response, next: NextFunction): void {}
|
|
|
187
180
|
*
|
|
188
181
|
* This middleware extracts the JWT token from the Authorization header (Bearer scheme)
|
|
189
182
|
* and stores it in `res.locals.tokens.access` for use by subsequent middleware (typically decodeAccess).
|
|
190
|
-
* It only processes requests that have `res.locals.route.isProtected` set to true.
|
|
183
|
+
* It only processes requests that have `res.locals.route.isProtected` or `res.locals.route.protected` set to true.
|
|
191
184
|
* For non-protected routes, it simply passes control to the next middleware.
|
|
192
185
|
*
|
|
193
186
|
* @param {Request} req - The Express request object. Should contain:
|
|
194
187
|
* - `req.headers.authorization`: Authorization header with Bearer token format
|
|
195
188
|
* @param {Response} res - The Express response object. Should contain:
|
|
196
|
-
* - `res.locals.route.isProtected`: Boolean flag to determine if route requires JWT protection
|
|
189
|
+
* - `res.locals.route.isProtected` or `res.locals.route.protected`: Boolean flag to determine if route requires JWT protection
|
|
197
190
|
* Parsed token will be added to `res.locals.tokens.access`
|
|
198
191
|
* @param {NextFunction} next - The next middleware function to be called
|
|
199
192
|
*
|
|
@@ -215,8 +208,8 @@ function parseBearer(req: Request, res: Response, next: NextFunction): void {}
|
|
|
215
208
|
* This middleware validates the JWT access token from `res.locals.tokens.access`,
|
|
216
209
|
* verifies its signature and structure, validates the issuer (iss) claim,
|
|
217
210
|
* and stores the decoded token in `res.locals.tokens.decodedAccess` for use by
|
|
218
|
-
* subsequent middleware. It only processes requests that have `res.locals.route.isProtected`
|
|
219
|
-
* set to true. For non-protected routes, it simply passes control to the next middleware.
|
|
211
|
+
* subsequent middleware. It only processes requests that have `res.locals.route.isProtected`
|
|
212
|
+
* or `res.locals.route.protected` set to true. For non-protected routes, it simply passes control to the next middleware.
|
|
220
213
|
*
|
|
221
214
|
* Note: By default, this middleware checks token expiration (exp claim) and will reject
|
|
222
215
|
* expired tokens. For token refresh flows where you need to identify the user even after
|
|
@@ -225,7 +218,7 @@ function parseBearer(req: Request, res: Response, next: NextFunction): void {}
|
|
|
225
218
|
*
|
|
226
219
|
* @param {Request} _req - The Express request object (unused)
|
|
227
220
|
* @param {Response} res - The Express response object. Should contain:
|
|
228
|
-
* - `res.locals.route.isProtected`: Boolean flag to determine if route requires JWT protection
|
|
221
|
+
* - `res.locals.route.isProtected` or `res.locals.route.protected`: Boolean flag to determine if route requires JWT protection
|
|
229
222
|
* - `res.locals.tokens.access`: The JWT token to decode (from parseBearer middleware)
|
|
230
223
|
* - `res.locals.tokens.ignoreExpiration`: Optional boolean to skip expiration checking (default: false)
|
|
231
224
|
* Decoded token will be added to `res.locals.tokens.decodedAccess`
|
|
@@ -331,16 +324,16 @@ req.body.rows[0].refreshToken = rt;
|
|
|
331
324
|
|
|
332
325
|
### JWT Decoding
|
|
333
326
|
|
|
334
|
-
#### Route Protection with isProtected
|
|
327
|
+
#### Route Protection with isProtected / protected
|
|
335
328
|
|
|
336
|
-
The `parseBearer()` and `decodeAccess()` middlewares only process requests when `res.locals.route.isProtected` is set to `true`. This allows you to selectively protect routes that require authentication.
|
|
329
|
+
The `parseBearer()` and `decodeAccess()` middlewares only process requests when `res.locals.route.isProtected` or `res.locals.route.protected` is set to `true`. This allows you to selectively protect routes that require authentication.
|
|
337
330
|
|
|
338
|
-
You should set
|
|
331
|
+
You should set one of these flags in a middleware before calling these functions:
|
|
339
332
|
|
|
340
333
|
```Javascript
|
|
341
334
|
// Example middleware to mark route as protected
|
|
342
335
|
function protectRoute(req, res, next) {
|
|
343
|
-
res.locals.route = { isProtected: true };
|
|
336
|
+
res.locals.route = { isProtected: true }; // or { protected: true }
|
|
344
337
|
next();
|
|
345
338
|
}
|
|
346
339
|
|
|
@@ -348,7 +341,7 @@ function protectRoute(req, res, next) {
|
|
|
348
341
|
router.get('/protected-route', protectRoute, tk.parseBearer, tk.decodeAccess, yourHandler);
|
|
349
342
|
```
|
|
350
343
|
|
|
351
|
-
If `res.locals.route.isProtected`
|
|
344
|
+
If both `res.locals.route.isProtected` and `res.locals.route.protected` are `false`, `undefined`, or `null`, these middlewares will simply call `next()` without processing the token, allowing the request to continue to the next middleware.
|
|
352
345
|
|
|
353
346
|
#### Access Token Processing
|
|
354
347
|
|
|
@@ -385,8 +378,6 @@ The decoded token is then stored in `res.locals.tokens.decodedAccess`:
|
|
|
385
378
|
res.locals.tokens.decodedAccess = decodedToken;
|
|
386
379
|
```
|
|
387
380
|
|
|
388
|
-
**Important:** This middleware **ignores token expiration** by design. This allows expired access tokens to be decoded, which is useful for token refresh flows where you need to identify the user even after their access token has expired.
|
|
389
|
-
|
|
390
381
|
**Note:** You should use both middlewares in sequence for full access token processing, or you can use just `parseBearer()` if you only need to extract the token without decoding it.
|
|
391
382
|
|
|
392
383
|
#### Refresh Token Decoding
|
|
@@ -410,13 +401,19 @@ res.locals.tokens.decodedRefresh = decodedToken;
|
|
|
410
401
|
|
|
411
402
|
**Token-express.js** uses **[@dwtechs/Winstan](https://www.npmjs.com/package/@dwtechs/winstan)** library for logging.
|
|
412
403
|
All logs are in debug mode. Meaning they should not appear in production mode.
|
|
404
|
+
Log messages are passed as lazy functions (`() => string`) so string interpolation and serialization are skipped entirely when debug logging is disabled.
|
|
405
|
+
|
|
406
|
+
## Support
|
|
407
|
+
|
|
408
|
+
| Environment | Version |
|
|
409
|
+
| :---------- | :-----: |
|
|
410
|
+
| Node.js | >= 22 |
|
|
413
411
|
|
|
414
412
|
## Contributors
|
|
415
413
|
|
|
416
414
|
**Token-express.js** is still in development and we would be glad to get all the help you can provide.
|
|
417
415
|
To contribute please read **[contributor.md](https://github.com/DWTechs/Token-express.js/blob/main/contributor.md)** for detailed installation guide.
|
|
418
416
|
|
|
419
|
-
|
|
420
417
|
## Stack
|
|
421
418
|
|
|
422
419
|
| Purpose | Choice | Motivation |
|
package/dist/toker-express.js
CHANGED
|
@@ -34,15 +34,18 @@ if (!TOKEN_SECRET)
|
|
|
34
34
|
throw new Error(`${LOGS_PREFIX}Missing TOKEN_SECRET environment variable`);
|
|
35
35
|
if (!isString(TOKEN_SECRET, "!0"))
|
|
36
36
|
throw new Error(`${LOGS_PREFIX}Invalid TOKEN_SECRET environment variable`);
|
|
37
|
+
if (TOKEN_SECRET.length < 32)
|
|
38
|
+
throw new Error(`${LOGS_PREFIX}TOKEN_SECRET must be at least 32 characters`);
|
|
37
39
|
const secrets = [TOKEN_SECRET];
|
|
38
40
|
const accessDuration = isNumber(ACCESS_TOKEN_DURATION, false) ? Number(ACCESS_TOKEN_DURATION) : 600;
|
|
39
41
|
const refreshDuration = isNumber(REFRESH_TOKEN_DURATION, false) ? Number(REFRESH_TOKEN_DURATION) : 86400;
|
|
40
42
|
function createTokens(req, res, next) {
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return
|
|
45
|
-
|
|
43
|
+
const iss = res.locals?.user?.id;
|
|
44
|
+
if (!isValidInteger(iss, 1, 999999999, false)) {
|
|
45
|
+
next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
log.debug(() => `${LOGS_PREFIX}Create tokens for user ${iss}`);
|
|
46
49
|
let at;
|
|
47
50
|
let rt;
|
|
48
51
|
try {
|
|
@@ -50,19 +53,21 @@ function createTokens(req, res, next) {
|
|
|
50
53
|
rt = sign(iss, refreshDuration, "refresh", secrets);
|
|
51
54
|
}
|
|
52
55
|
catch (err) {
|
|
53
|
-
|
|
56
|
+
next(err);
|
|
57
|
+
return;
|
|
54
58
|
}
|
|
55
|
-
log.debug(
|
|
59
|
+
log.debug(() => `${LOGS_PREFIX}Tokens created for user ${iss}`);
|
|
56
60
|
req.body.rows[0].accessToken = at;
|
|
57
61
|
req.body.rows[0].refreshToken = rt;
|
|
58
62
|
next();
|
|
59
63
|
}
|
|
60
64
|
function refreshTokens(req, res, next) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
return
|
|
65
|
-
|
|
65
|
+
const iss = res.locals?.tokens?.decodedAccess?.iss;
|
|
66
|
+
if (!isValidInteger(iss, 1, 999999999, false)) {
|
|
67
|
+
next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
log.debug(() => `${LOGS_PREFIX}Create tokens for user ${iss}`);
|
|
66
71
|
let at;
|
|
67
72
|
let rt;
|
|
68
73
|
try {
|
|
@@ -70,65 +75,81 @@ function refreshTokens(req, res, next) {
|
|
|
70
75
|
rt = sign(iss, refreshDuration, "refresh", secrets);
|
|
71
76
|
}
|
|
72
77
|
catch (err) {
|
|
73
|
-
|
|
78
|
+
next(err);
|
|
79
|
+
return;
|
|
74
80
|
}
|
|
75
|
-
log.debug(
|
|
81
|
+
log.debug(() => `${LOGS_PREFIX}Tokens refreshed for user ${iss}`);
|
|
76
82
|
req.body.rows[0].accessToken = at;
|
|
77
83
|
req.body.rows[0].refreshToken = rt;
|
|
78
84
|
next();
|
|
79
85
|
}
|
|
80
86
|
function parseBearer(req, res, next) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
return
|
|
84
|
-
|
|
87
|
+
if (!res.locals?.route?.isProtected && !res.locals?.route?.protected) {
|
|
88
|
+
next();
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
log.debug(() => `${LOGS_PREFIX}parse bearer to get access token`);
|
|
85
92
|
try {
|
|
86
|
-
res.locals.tokens
|
|
93
|
+
res.locals.tokens ??= {};
|
|
94
|
+
res.locals.tokens.access = parseBearer$1(req.headers.authorization);
|
|
87
95
|
}
|
|
88
96
|
catch (e) {
|
|
89
|
-
|
|
97
|
+
next(e);
|
|
98
|
+
return;
|
|
90
99
|
}
|
|
91
100
|
next();
|
|
92
101
|
}
|
|
93
102
|
function decodeAccess(_req, res, next) {
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
return
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
103
|
+
log.debug(() => `${LOGS_PREFIX}decode access token`);
|
|
104
|
+
if (!res.locals?.route?.isProtected && !res.locals?.route?.protected) {
|
|
105
|
+
next();
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const t = res.locals?.tokens?.access;
|
|
109
|
+
const ignoreExpiration = res.locals?.tokens?.ignoreExpiration ?? false;
|
|
110
|
+
if (!isJWT(t)) {
|
|
111
|
+
next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid access token` });
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
102
114
|
let dt = null;
|
|
103
115
|
try {
|
|
104
116
|
dt = verify(t, secrets, ignoreExpiration);
|
|
105
117
|
}
|
|
106
118
|
catch (e) {
|
|
107
|
-
|
|
119
|
+
next(e);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!isValidInteger(dt.iss, 1, 999999999, false)) {
|
|
123
|
+
next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
|
|
124
|
+
return;
|
|
108
125
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
res.locals.tokens = Object.assign(Object.assign({}, res.locals.tokens), { decodedAccess: dt });
|
|
126
|
+
log.debug(() => `${LOGS_PREFIX}Access token decoded for user ${dt.iss}`);
|
|
127
|
+
res.locals.tokens ??= {};
|
|
128
|
+
res.locals.tokens.decodedAccess = dt;
|
|
113
129
|
next();
|
|
114
130
|
}
|
|
115
131
|
function decodeRefresh(req, res, next) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
132
|
+
const t = req.body?.refreshToken;
|
|
133
|
+
log.debug(() => `${LOGS_PREFIX}Decoding refresh token`);
|
|
134
|
+
if (!isJWT(t)) {
|
|
135
|
+
next({ statusCode: 401, message: `${LOGS_PREFIX}Invalid refresh token` });
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
121
138
|
let dt = null;
|
|
122
139
|
try {
|
|
123
140
|
dt = verify(t, secrets, false);
|
|
124
141
|
}
|
|
125
142
|
catch (e) {
|
|
126
|
-
|
|
143
|
+
next(e);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
if (!isValidInteger(dt.iss, 1, 999999999, false)) {
|
|
147
|
+
next({ statusCode: 400, message: `${LOGS_PREFIX}Missing iss` });
|
|
148
|
+
return;
|
|
127
149
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
res.locals.tokens = Object.assign(Object.assign({}, res.locals.tokens), { decodedRefresh: dt });
|
|
150
|
+
log.debug(() => `${LOGS_PREFIX}Refresh token decoded for user ${dt.iss}`);
|
|
151
|
+
res.locals.tokens ??= {};
|
|
152
|
+
res.locals.tokens.decodedRefresh = dt;
|
|
132
153
|
next();
|
|
133
154
|
}
|
|
134
155
|
|
package/package.json
CHANGED
|
@@ -1,35 +1,41 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dwtechs/toker-express",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.3",
|
|
4
|
+
"type": "module",
|
|
4
5
|
"description": "Open source JWT management library for Express.js to refresh and decode tokens safely.",
|
|
5
6
|
"keywords": [
|
|
6
7
|
"JWT",
|
|
7
8
|
"Express"
|
|
8
9
|
],
|
|
9
10
|
"homepage": "https://github.com/DWTechs/Toker-express.js",
|
|
10
|
-
"main": "dist/toker-express",
|
|
11
|
-
"types": "dist/toker-express",
|
|
11
|
+
"main": "dist/toker-express.js",
|
|
12
|
+
"types": "dist/toker-express.d.ts",
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/toker-express.js",
|
|
16
|
+
"types": "./dist/toker-express.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
12
19
|
"repository": {
|
|
13
20
|
"type": "git",
|
|
14
21
|
"url": "https://github.com/DWTechs/Toker-express.js"
|
|
15
22
|
},
|
|
16
23
|
"bugs": {
|
|
17
|
-
"url": "https://github.com/DWTechs/Toker-express.js/issues"
|
|
18
|
-
"email": ""
|
|
24
|
+
"url": "https://github.com/DWTechs/Toker-express.js/issues"
|
|
19
25
|
},
|
|
20
26
|
"license": "MIT",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=22"
|
|
29
|
+
},
|
|
21
30
|
"author": {
|
|
22
31
|
"name": "Ludovic Cluber",
|
|
23
32
|
"email": "http://www.lcluber.com/contact",
|
|
24
33
|
"url": "http://www.lcluber.com"
|
|
25
34
|
},
|
|
26
|
-
"contributors": [],
|
|
27
35
|
"scripts": {
|
|
28
|
-
"start": "",
|
|
29
36
|
"prebuild": "npm install",
|
|
30
37
|
"build": "node ./scripts/clear && tsc && npm run rollup && node ./scripts/copy && npm run test",
|
|
31
38
|
"rollup:mjs": "rollup --config rollup.config.mjs",
|
|
32
|
-
"rollup:cjs": "rollup --config rollup.config.cjs.mjs",
|
|
33
39
|
"rollup": "npm run rollup:mjs",
|
|
34
40
|
"test": "jest --coverage"
|
|
35
41
|
},
|
|
@@ -38,17 +44,15 @@
|
|
|
38
44
|
],
|
|
39
45
|
"dependencies": {
|
|
40
46
|
"@dwtechs/checkard": "3.6.0",
|
|
41
|
-
"@dwtechs/toker": "0.1
|
|
42
|
-
"@dwtechs/winstan": "0.
|
|
47
|
+
"@dwtechs/toker": "0.2.1",
|
|
48
|
+
"@dwtechs/winstan": "0.7.0"
|
|
43
49
|
},
|
|
44
50
|
"devDependencies": {
|
|
45
51
|
"@babel/preset-env": "7.26.0",
|
|
46
|
-
"@
|
|
47
|
-
"@types/express": "5.0.3",
|
|
52
|
+
"@types/express": "5.0.6",
|
|
48
53
|
"babel-jest": "29.7.0",
|
|
49
|
-
"core-js": "3.38.1",
|
|
50
54
|
"jest": "29.7.0",
|
|
51
55
|
"rollup": "4.24.0",
|
|
52
|
-
"typescript": "
|
|
56
|
+
"typescript": "6.0.3"
|
|
53
57
|
}
|
|
54
58
|
}
|