@dwtechs/toker-express 0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 DWTechs
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 ADDED
@@ -0,0 +1,268 @@
1
+
2
+ [![License: MIT](https://img.shields.io/npm/l/@dwtechs/toker-express.svg?color=brightgreen)](https://opensource.org/licenses/MIT)
3
+ [![npm version](https://badge.fury.io/js/%40dwtechs%2Ftoker-express.svg)](https://www.npmjs.com/package/@dwtechs/toker-express)
4
+ [![last version release date](https://img.shields.io/github/release-date/DWTechs/Toker-express.js)](https://www.npmjs.com/package/@dwtechs/toker-express)
5
+
6
+
7
+ - [Synopsis](#synopsis)
8
+ - [Support](#support)
9
+ - [Installation](#installation)
10
+ - [Usage](#usage)
11
+ - [Environment variables](#environment-variables)
12
+ - [API Reference](#api-reference)
13
+ - [Logs](#logs)
14
+ - [Contributors](#contributors)
15
+ - [Stack](#stack)
16
+
17
+
18
+ ## Synopsis
19
+
20
+ **[Toker-express.js](https://github.com/DWTechs/Toker-express.js)** is an open source JWT management library for Express.js to refresh and decode tokens safely.
21
+ It includes @dwtechs/toker library and adds Express middlewares to be used in a node.js service.
22
+
23
+ - ๐Ÿชถ Very lightweight
24
+ - ๐Ÿงช Thoroughly tested
25
+ - ๐Ÿšš Shipped as EcmaScrypt Express module
26
+ - ๐Ÿ“ Written in Typescript
27
+
28
+
29
+ ## Support
30
+
31
+ - node: 22
32
+
33
+ This is the oldest targeted versions.
34
+
35
+
36
+ ## Installation
37
+
38
+ ```bash
39
+ $ npm i @dwtechs/toker-express
40
+ ```
41
+
42
+
43
+ ## Usage
44
+
45
+
46
+ ```javascript
47
+
48
+ // @ts-check
49
+ import * as tk from "@dwtechs/Toker-express";
50
+ import express from "express";
51
+ const router = express.Router();
52
+
53
+ import cEntity from "../entities/consumer.js";
54
+ import uEntity from "../entities/user.js";
55
+ import checkToken from "../middlewares/validators/check-token.js";
56
+ import login from "../middlewares/login.js";
57
+
58
+ const add = [
59
+ uEntity.normalize,
60
+ uEntity.validate,
61
+ login,
62
+ tk.refresh,
63
+ cEntity.add,
64
+ ];
65
+
66
+ const refresh = [
67
+ cEntity.validate,
68
+ tk.decodeAccess,
69
+ tk.decodeRefresh,
70
+ checkToken,
71
+ tk.refresh,
72
+ cEntity.update,
73
+ ];
74
+
75
+ const del = [
76
+ checkToken,
77
+ tk.decodeAccess,
78
+ cEntity.delete,
79
+ ];
80
+
81
+ // Routes
82
+
83
+ // add a consumer. Log a user
84
+ router.post("/", add);
85
+
86
+ // Update a consumer with new tokens
87
+ // Used for login and refresh tokens
88
+ router.put("/", refresh);
89
+
90
+ // delete a consumer. Used when logging out
91
+ router.delete("/", del);
92
+
93
+
94
+ ```
95
+
96
+
97
+ ### Environment variables
98
+
99
+ You can intialise the library using the following environment variables:
100
+
101
+ ```bash
102
+ ACCESS_TOKEN_DURATION,
103
+ REFRESH_TOKEN_DURATION
104
+ TOKEN_SECRET,
105
+ ```
106
+
107
+ These environment variables will update the default values of the lib at start up.
108
+ So you do not need to init the library in the code.
109
+
110
+ Note that **TOKEN_SECRET** is mandatory.
111
+
112
+ Default values :
113
+
114
+ ```javascript
115
+ const accessDuration = isNumber(ACCESS_TOKEN_DURATION, false) ? ACCESS_TOKEN_DURATION : 600; // #10 * 60 => 10 mins
116
+ const refreshDuration = isNumber(REFRESH_TOKEN_DURATION, false) ? REFRESH_TOKEN_DURATION : 86400; // #24 * 60 * 60 => 1 day
117
+ ```
118
+
119
+ ## API Reference
120
+
121
+
122
+ ```typescript
123
+
124
+ /**
125
+ * Refreshes the JWT tokens for a user.
126
+ *
127
+ * This function generates new access and refresh tokens for a user based on the provided
128
+ * decoded access token or user ID in the request body. It validates the issuer (iss) and
129
+ * creates new tokens if the validation is successful. The new tokens are then added to the
130
+ * response object.
131
+ *
132
+ * @param {Request} req - The request object containing the decoded access token or user ID.
133
+ * @param {MyResponse} res - The response object where the new tokens will be added.
134
+ * @param {NextFunction} next - The next middleware function in the Express.js request-response cycle.
135
+ *
136
+ * @returns {Promise<void>} Calls the next middleware function with an error if the issuer is invalid,
137
+ * otherwise proceeds to the next middleware function.
138
+ *
139
+ * @throws {InvalidIssuerError} If the issuer (iss) is not a string or number (HTTP 400)
140
+ * @throws {InvalidSecretsError} If the secrets array is empty or invalid (HTTP 500)
141
+ * @throws {InvalidDurationError} If the duration is not a positive number (HTTP 400)
142
+ * @throws {InvalidBase64Secret} If the secret cannot be decoded from base64 (HTTP 500)
143
+ * @throws {Object} Will call next() with error object containing:
144
+ * - statusCode: 400 - When iss (issuer) is missing or invalid
145
+ * - statusCode: 400 - When iss is not a valid number between 1 and 999999999
146
+ */
147
+ function refresh(req: Request, res: MyResponse, next: NextFunction): void {}
148
+
149
+ /**
150
+ * Express middleware function to decode and verify an access token from the Authorization header.
151
+ *
152
+ * This middleware extracts the JWT access token from the Authorization header, validates its format,
153
+ * verifies its signature, and attaches the decoded token to the request object for use by subsequent
154
+ * middleware. It only processes requests that have `req.isProtected` set to true.
155
+ *
156
+ * @param {Request} req - The Express request object containing the Authorization header
157
+ * @param {Response} _res - The Express response object (not used in this function)
158
+ * @param {NextFunction} next - The next middleware function to be called
159
+ *
160
+ * @returns {void} Calls the next middleware function, either with an error or successfully
161
+ *
162
+ * @throws {MissingAuthorizationError} If the Authorization header is missing (HTTP 401)
163
+ * @throws {InvalidBearerFormatError} If the Authorization header format is invalid (HTTP 401)
164
+ * @throws {InvalidTokenError} If the token is malformed or has invalid structure (HTTP 401)
165
+ * @throws {ExpiredTokenError} If the token has expired (exp claim) (HTTP 401)
166
+ * @throws {InactiveTokenError} If the token cannot be used yet (nbf claim) (HTTP 401)
167
+ * @throws {InvalidSignatureError} If the token signature is invalid (HTTP 401)
168
+ * @throws {InvalidSecretsError} If the secrets configuration is invalid (HTTP 500)
169
+ * @throws {InvalidBase64Secret} If the secret cannot be decoded from base64 (HTTP 500)
170
+ * @throws {Object} Will call next() with error object containing:
171
+ * - statusCode: 401 - When token is not a valid JWT format
172
+ * - statusCode: 400 - When decoded token is missing required 'iss' claim
173
+ *
174
+ * @example
175
+ * ```typescript
176
+ * // Usage in Express route with protection middleware
177
+ * const protect = (req: Request, res: Response, next: NextFunction) => {
178
+ * req.isProtected = true;
179
+ * next();
180
+ * };
181
+ *
182
+ */
183
+ function decodeAccess(req: Request, _res: Response, next: NextFunction): void {}
184
+
185
+ /**
186
+ * Middleware function to decode and verify a refresh token from the request body.
187
+ *
188
+ * @param {Request} req - The request object containing the refresh token in the body.
189
+ * @param {Response} _res - The response object (not used in this function).
190
+ * @param {NextFunction} next - The next middleware function to be called.
191
+ *
192
+ * @returns {Promise<void>} Calls the next middleware function with an error object if the token is invalid or missing required fields.
193
+ *
194
+ * @throws {InvalidTokenError} If the token is malformed or has invalid structure (HTTP 401)
195
+ * @throws {InvalidSecretsError} If the secrets configuration is invalid (HTTP 500)
196
+ * @throws {ExpiredTokenError} If the refresh token has expired (exp claim) (HTTP 401)
197
+ * @throws {InactiveTokenError} If the token cannot be used yet (nbf claim) (HTTP 401)
198
+ * @throws {InvalidSignatureError} If the token signature is invalid (HTTP 401)
199
+ * @throws {InvalidBase64Secret} If the secret cannot be decoded from base64 (HTTP 500)
200
+ * @throws {Object} Will call next() with error object containing:
201
+ * - statusCode: 401 - When refresh token is not a valid JWT format
202
+ * - statusCode: 400 - When decoded token is missing required 'iss' claim
203
+ */
204
+ function decodeRefresh(req: Request, _res: Response, next: NextFunction): void {}
205
+
206
+ ```
207
+
208
+ ### JWT Refresh
209
+
210
+ This function will look for an ISS in the client request body :
211
+
212
+ ```Javascript
213
+ const iss = req.body.decodedAccessToken?.iss || req.body?.id?.toString();
214
+ ```
215
+
216
+ It will then send both new refresh and access tokens in the res object.
217
+
218
+ ```Javascript
219
+ res.rows = [{ accessToken, refreshToken }];
220
+ ```
221
+
222
+ ### JWT Decoding
223
+
224
+ decodeAccess() functions will look for a bearer in authorization headers.
225
+
226
+ ```Javascript
227
+ const bearer = req.headers.authorization;
228
+ ```
229
+
230
+ It will then send the decoded token in the res object.
231
+
232
+ ```Javascript
233
+ req.decodedAccessToken = decodedToken;
234
+ ```
235
+
236
+ decodeRefresh() functions will look for a token in the client request body.
237
+
238
+ ```Javascript
239
+ const token = req.body.refreshToken;
240
+ ```
241
+
242
+ It will then send the decoded token in the res object.
243
+
244
+ ```Javascript
245
+ req.decodedRefreshToken = decodedToken;
246
+ ```
247
+
248
+
249
+ ## Logs
250
+
251
+ **Token-express.js** uses **[@dwtechs/Winstan](https://www.npmjs.com/package/@dwtechs/winstan)** library for logging.
252
+ All logs are in debug mode. Meaning they should not appear in production mode.
253
+
254
+ ## Contributors
255
+
256
+ **Token-express.js** is still in development and we would be glad to get all the help you can provide.
257
+ To contribute please read **[contributor.md](https://github.com/DWTechs/Token-express.js/blob/main/contributor.md)** for detailed installation guide.
258
+
259
+
260
+ ## Stack
261
+
262
+ | Purpose | Choice | Motivation |
263
+ | :-------------- | :------------------------------------------: | -------------------------------------------------------------: |
264
+ | repository | [Github](https://github.com/) | hosting for software development version control using Git |
265
+ | package manager | [npm](https://www.npmjs.com/get-npm) | default node.js package manager |
266
+ | language | [TypeScript](https://www.typescriptlang.org) | static type checking along with the latest ECMAScript features |
267
+ | module bundler | [Rollup](https://rollupjs.org) | advanced module bundler for ES6 modules |
268
+ | unit testing | [Jest](https://jestjs.io/) | delightful testing with a focus on simplicity |
@@ -0,0 +1,51 @@
1
+ /*
2
+ MIT License
3
+
4
+ Copyright (c) 2025 DWTechs
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
24
+ https://github.com/DWTechs/Toker-express.js
25
+ */
26
+
27
+ import type { Request, Response, NextFunction } from 'express';
28
+ import type { MyResponse } from './interfaces';
29
+
30
+ // Extend Express Request interface globally
31
+ declare global {
32
+ namespace Express {
33
+ interface Request {
34
+ isProtected?: boolean;
35
+ decodedAccessToken?: any;
36
+ decodedRefreshToken?: any;
37
+ }
38
+ }
39
+ }
40
+
41
+ declare function refresh(req: Request, res: MyResponse, next: NextFunction): Promise<void>;
42
+ declare function decodeAccess(req: Request, _res: Response, next: NextFunction): void;
43
+ declare function decodeRefresh(req: Request, _res: Response, next: NextFunction): Promise<void>;
44
+
45
+ export {
46
+ refresh,
47
+ decodeAccess,
48
+ decodeRefresh,
49
+ };
50
+
51
+
@@ -0,0 +1,118 @@
1
+ /*
2
+ MIT License
3
+
4
+ Copyright (c) 2025 DWTechs
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
24
+ https://github.com/DWTechs/Toker-express.js
25
+ */
26
+
27
+ import { sign, parseBearer, verify } from '@dwtechs/toker';
28
+ import { isString, isNumber, isValidNumber, isJWT } from '@dwtechs/checkard';
29
+ import { log } from '@dwtechs/winstan';
30
+
31
+ var __awaiter = (undefined && undefined.__awaiter) || function (thisArg, _arguments, P, generator) {
32
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
33
+ return new (P || (P = Promise))(function (resolve, reject) {
34
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
35
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
36
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
37
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
38
+ });
39
+ };
40
+ const { TOKEN_SECRET, ACCESS_TOKEN_DURATION, REFRESH_TOKEN_DURATION } = process.env;
41
+ const TE_PREFIX = "Toker-express: ";
42
+ if (!TOKEN_SECRET)
43
+ throw new Error(`${TE_PREFIX} Missing TOKEN_SECRET environment variable`);
44
+ if (!isString(TOKEN_SECRET, "!0"))
45
+ throw new Error(`${TE_PREFIX} Invalid TOKEN_SECRET environment variable`);
46
+ const secrets = [TOKEN_SECRET];
47
+ const accessDuration = isNumber(ACCESS_TOKEN_DURATION, false) ? ACCESS_TOKEN_DURATION : 600;
48
+ const refreshDuration = isNumber(REFRESH_TOKEN_DURATION, false) ? REFRESH_TOKEN_DURATION : 86400;
49
+ function refresh(req, res, next) {
50
+ return __awaiter(this, void 0, void 0, function* () {
51
+ var _a, _b, _c;
52
+ const iss = ((_a = req.decodedAccessToken) === null || _a === void 0 ? void 0 : _a.iss) || ((_c = (_b = req.body) === null || _b === void 0 ? void 0 : _b.id) === null || _c === void 0 ? void 0 : _c.toString());
53
+ if (!isValidNumber(iss, 1, 999999999, false))
54
+ return next({ statusCode: 400, message: `${TE_PREFIX} Missing iss` });
55
+ log.debug(`Create tokens for user ${iss}`);
56
+ let accessToken;
57
+ let refreshToken;
58
+ try {
59
+ accessToken = sign(iss, accessDuration, "access", secrets);
60
+ refreshToken = sign(iss, refreshDuration, "refresh", secrets);
61
+ }
62
+ catch (err) {
63
+ return next(err);
64
+ }
65
+ log.debug(`refreshToken='${refreshToken}', accessToken='${accessToken}'`);
66
+ res.rows = [{ accessToken, refreshToken }];
67
+ next();
68
+ });
69
+ }
70
+ function decodeAccess(req, _res, next) {
71
+ log.debug(`decode access token`);
72
+ if (!req.isProtected)
73
+ return next();
74
+ let t;
75
+ try {
76
+ t = parseBearer(req.headers.authorization);
77
+ }
78
+ catch (e) {
79
+ return next(e);
80
+ }
81
+ log.debug(`accessToken : ${t}`);
82
+ if (!isJWT(t))
83
+ return next({ statusCode: 401, message: `${TE_PREFIX} Invalid access token` });
84
+ let decodedToken = null;
85
+ try {
86
+ decodedToken = verify(t, secrets, true);
87
+ }
88
+ catch (e) {
89
+ return next(e);
90
+ }
91
+ if (!isValidNumber(decodedToken.iss, 1, 999999999, false))
92
+ return next({ statusCode: 400, message: `${TE_PREFIX} Missing iss` });
93
+ log.debug(`Decoded access token : ${JSON.stringify(decodedToken)}`);
94
+ req.decodedAccessToken = decodedToken;
95
+ next();
96
+ }
97
+ function decodeRefresh(req, _res, next) {
98
+ return __awaiter(this, void 0, void 0, function* () {
99
+ const token = req.body.refreshToken;
100
+ log.debug(`decodeRefresh(token=${token})`);
101
+ if (!isJWT(token))
102
+ return next({ statusCode: 401, message: `${TE_PREFIX} Invalid refresh token` });
103
+ let decodedToken = null;
104
+ try {
105
+ decodedToken = verify(token, secrets, false);
106
+ }
107
+ catch (e) {
108
+ return next(e);
109
+ }
110
+ if (!isValidNumber(decodedToken.iss, 1, 999999999, false))
111
+ return next({ statusCode: 400, message: `${TE_PREFIX} Missing iss` });
112
+ log.debug(`Decoded refresh token : ${JSON.stringify(req.decodedRefreshToken)}`);
113
+ req.decodedRefreshToken = decodedToken;
114
+ next();
115
+ });
116
+ }
117
+
118
+ export { decodeAccess, decodeRefresh, refresh };
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "@dwtechs/toker-express",
3
+ "version": "0.1.0",
4
+ "description": "Open source JWT management library for Express.js to refresh and decode tokens safely.",
5
+ "keywords": [
6
+ "JWT",
7
+ "Express"
8
+ ],
9
+ "homepage": "https://github.com/DWTechs/Toker-express.js",
10
+ "main": "dist/toker-express",
11
+ "types": "dist/toker-express",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/DWTechs/Toker-express.js"
15
+ },
16
+ "bugs": {
17
+ "url": "https://github.com/DWTechs/Toker-express.js/issues",
18
+ "email": ""
19
+ },
20
+ "license": "MIT",
21
+ "author": {
22
+ "name": "Ludovic Cluber",
23
+ "email": "http://www.lcluber.com/contact",
24
+ "url": "http://www.lcluber.com"
25
+ },
26
+ "contributors": [],
27
+ "scripts": {
28
+ "start": "",
29
+ "prebuild": "npm install",
30
+ "build": "node ./scripts/clear && tsc && npm run rollup && node ./scripts/copy && npm run test",
31
+ "rollup:mjs": "rollup --config rollup.config.mjs",
32
+ "rollup:cjs": "rollup --config rollup.config.cjs.mjs",
33
+ "rollup": "npm run rollup:mjs",
34
+ "test": ""
35
+ },
36
+ "files": [
37
+ "dist/"
38
+ ],
39
+ "dependencies": {
40
+ "@dwtechs/checkard": "3.2.3",
41
+ "@dwtechs/toker": "0.1.0",
42
+ "@dwtechs/winstan": "0.3.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/express": "5.0.0",
46
+ "@rollup/plugin-node-resolve": "15.3.0",
47
+ "core-js": "3.38.1",
48
+ "rollup": "4.24.0",
49
+ "typescript": "5.6.3"
50
+ }
51
+ }