@dwtechs/toker 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 +21 -0
- package/README.md +315 -0
- package/dist/toker.d.ts +111 -0
- package/dist/toker.js +190 -0
- package/package.json +50 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022 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,315 @@
|
|
|
1
|
+
|
|
2
|
+
[](https://opensource.org/licenses/MIT)
|
|
3
|
+
[](https://www.npmjs.com/package/@dwtechs/toker)
|
|
4
|
+
[](https://www.npmjs.com/package/@dwtechs/toker)
|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
- [Synopsis](#synopsis)
|
|
8
|
+
- [Support](#support)
|
|
9
|
+
- [Installation](#installation)
|
|
10
|
+
- [Usage](#usage)
|
|
11
|
+
- [API Reference](#api-reference)
|
|
12
|
+
- [Error Handling](#error-handling)
|
|
13
|
+
- [options](#options)
|
|
14
|
+
- [Express.js](#expressjs)
|
|
15
|
+
- [Contributors](#contributors)
|
|
16
|
+
- [Stack](#stack)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
## Synopsis
|
|
20
|
+
|
|
21
|
+
**[Toker.js](https://github.com/DWTechs/Toker.js)** is an open source JWT management library for Node.js to sign, verify and parse bearer safely.
|
|
22
|
+
|
|
23
|
+
- ๐ฆ Only 1 dependency to check inputs variables
|
|
24
|
+
- ๐ชถ Very lightweight
|
|
25
|
+
- ๐งช Thoroughly tested
|
|
26
|
+
- ๐ Shipped as EcmaScrypt module
|
|
27
|
+
- ๐ Written in Typescript
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Support
|
|
31
|
+
|
|
32
|
+
- Node.js: 22
|
|
33
|
+
|
|
34
|
+
This is the oldest targeted versions.
|
|
35
|
+
The library uses node:crypto.
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
$ npm i @dwtechs/toker
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
Example of use with Express.js using ES6 module format
|
|
49
|
+
|
|
50
|
+
```javascript
|
|
51
|
+
|
|
52
|
+
import { compare, randomPwd, encrypt, sign, verify } from "@dwtechs/toker";
|
|
53
|
+
|
|
54
|
+
const { ACCESS_TOKEN_DURATION, REFRESH_TOKEN_DURATION, TOKEN_SECRET } = process.env;
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
function decodeAccessToken(req, res, next){
|
|
58
|
+
let accessToken: string;
|
|
59
|
+
try {
|
|
60
|
+
accessToken = parseBearer(req.headers.authorization);
|
|
61
|
+
} catch (err: any) {
|
|
62
|
+
return next(err);
|
|
63
|
+
}
|
|
64
|
+
let decodedToken = null;
|
|
65
|
+
try {
|
|
66
|
+
decodedToken = verify(accessToken, [TOKEN_SECRET], true);
|
|
67
|
+
} catch (err: any) {
|
|
68
|
+
return next(err);
|
|
69
|
+
}
|
|
70
|
+
req.body.decodedAccessToken = decodedToken;
|
|
71
|
+
next();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function refreshToken(req, res, next) {
|
|
75
|
+
const iss = req.body.decodedAccessToken?.iss;
|
|
76
|
+
const newAccessToken = sign(iss, ACCESS_TOKEN_DURATION, "access", secrets);
|
|
77
|
+
const newRefreshToken = sign(iss, REFRESH_TOKEN_DURATION, "refresh", secrets);
|
|
78
|
+
try {
|
|
79
|
+
res.jwt = sign(req.userId, 3600, "access", [TOKEN_SECRET]);
|
|
80
|
+
next();
|
|
81
|
+
catch(err: any) {
|
|
82
|
+
next(err);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
decodeAccessToken,
|
|
88
|
+
refreshToken,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
### Types
|
|
98
|
+
---
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
|
|
102
|
+
// JWT
|
|
103
|
+
type Type = "access" | "refresh";
|
|
104
|
+
|
|
105
|
+
type Header = {
|
|
106
|
+
alg: string,
|
|
107
|
+
typ: string,
|
|
108
|
+
kid: number,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
type Payload = {
|
|
112
|
+
iss: number | string,
|
|
113
|
+
iat: number,
|
|
114
|
+
nbf: number,
|
|
115
|
+
exp: number,
|
|
116
|
+
typ: Type,
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Methods
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
|
|
126
|
+
// Default values
|
|
127
|
+
const header {
|
|
128
|
+
alg: "HS256", // HMAC using SHA-256 hash algorithm
|
|
129
|
+
typ: "JWT", // JSON Web Token
|
|
130
|
+
kid: 0, // Random key ID
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Signs a JWT (JSON Web Token) with the given parameters.
|
|
135
|
+
*
|
|
136
|
+
* @param {number|string} iss - The issuer of the token, which can be a string or a number.
|
|
137
|
+
* @param {number} duration - The duration for which the token is valid, in seconds.
|
|
138
|
+
* @param {Type} type - The type of the token, either "access" or "refresh".
|
|
139
|
+
* @param {string[]} b64Keys - An array of base64 encoded secrets used for signing the token.
|
|
140
|
+
* @returns {string} The signed JWT as a string.
|
|
141
|
+
* @throws {InvalidIssuerError} Throws when `iss` is not a string or a number - HTTP 400
|
|
142
|
+
* @throws {InvalidSecretsError} Throws when `b64Keys` is not an array or is empty - HTTP 500
|
|
143
|
+
* @throws {InvalidDurationError} Throws when `duration` is not a positive number - HTTP 400
|
|
144
|
+
* @throws {SecretDecodingError} Throws when the secret cannot be decoded from base64 - HTTP 500
|
|
145
|
+
*
|
|
146
|
+
* // Examples that throw specific errors:
|
|
147
|
+
* sign(null, 3600, "access", secrets); // Throws InvalidIssuerError
|
|
148
|
+
* sign("user123", 3600, "access", []); // Throws InvalidSecretsError
|
|
149
|
+
* sign("user123", -1, "access", secrets); // Throws InvalidDurationError
|
|
150
|
+
* sign("user123", 3600, "access", ["invalid-base64!"]); // Throws SecretDecodingError
|
|
151
|
+
* ```
|
|
152
|
+
*/
|
|
153
|
+
function sign( iss: number | string,
|
|
154
|
+
duration: number,
|
|
155
|
+
type: Type,
|
|
156
|
+
b64Keys: string[]
|
|
157
|
+
): string {}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Verifies a JWT token using the provided base64-encoded secrets.
|
|
161
|
+
*
|
|
162
|
+
* @param {string} token - The JWT token to verify.
|
|
163
|
+
* @param {string[]} b64Keys - An array of base64-encoded secrets used for verification.
|
|
164
|
+
* @param {boolean} ignoreExpiration - Optional flag to ignore the expiration time of the token. Defaults to false.
|
|
165
|
+
* @returns {Payload} The decoded payload of the JWT token.
|
|
166
|
+
* @throws {InvalidTokenError} Throws when the token is malformed, has invalid structure, algorithm, or type - HTTP 401
|
|
167
|
+
* @throws {InvalidSecretsError} Throws when b64Keys is not an array or is empty - HTTP 500
|
|
168
|
+
* @throws {TokenNotActiveError} Throws when the token cannot be used yet (nbf claim) - HTTP 401
|
|
169
|
+
* @throws {TokenExpiredError} Throws when the token has expired (exp claim) - HTTP 401
|
|
170
|
+
* @throws {SecretDecodingError} Throws when the secret is not valid base64 encoded - HTTP 500
|
|
171
|
+
* @throws {InvalidSignatureError} Throws when the token signature is invalid - HTTP 401
|
|
172
|
+
*
|
|
173
|
+
* // Examples that throw specific errors:
|
|
174
|
+
* verify("invalid.token", secrets); // Throws InvalidTokenError
|
|
175
|
+
* verify(validToken, []); // Throws InvalidSecretsError
|
|
176
|
+
* verify(expiredToken, secrets); // Throws TokenExpiredError
|
|
177
|
+
* verify(futureToken, secrets); // Throws TokenNotActiveError
|
|
178
|
+
* verify(tamperedToken, secrets); // Throws InvalidSignatureError
|
|
179
|
+
* verify(validToken, ["invalid-base64!"]); // Throws SecretDecodingError
|
|
180
|
+
* ```
|
|
181
|
+
*/
|
|
182
|
+
function verify( token: string,
|
|
183
|
+
b64Keys: string[],
|
|
184
|
+
ignoreExpiration = false
|
|
185
|
+
): Payload {}
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Extracts the JWT token from an HTTP Authorization header with Bearer authentication scheme.
|
|
190
|
+
*
|
|
191
|
+
* This function validates that the authorization header follows the correct Bearer token format
|
|
192
|
+
* ("Bearer <token>") and extracts the token portion for further processing.
|
|
193
|
+
*
|
|
194
|
+
* @param {string | undefined} authorization - The Authorization header value from an HTTP request
|
|
195
|
+
* @returns {string} The extracted JWT token as a string
|
|
196
|
+
* @throws {MissingAuthorizationError} Throws when the authorization parameter is undefined - HTTP 401
|
|
197
|
+
* @throws {InvalidBearerFormatError} Throws when the format is invalid - HTTP 401
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```typescript
|
|
201
|
+
* import { parseBearer, MissingAuthorizationError, InvalidBearerFormatError } from "@dwtechs/passken";
|
|
202
|
+
*
|
|
203
|
+
* // Valid Bearer tokens
|
|
204
|
+
* const validHeader = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
|
|
205
|
+
* const token = parseBearer(validHeader);
|
|
206
|
+
* // Returns: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
|
|
207
|
+
*
|
|
208
|
+
* // Handles multiple spaces
|
|
209
|
+
* const headerWithSpaces = "Bearer token123";
|
|
210
|
+
* const token2 = parseBearer(headerWithSpaces);
|
|
211
|
+
* // Returns: "token123"
|
|
212
|
+
*
|
|
213
|
+
* // Examples that throw specific errors:
|
|
214
|
+
* parseBearer(undefined); // Throws MissingAuthorizationError: "Authorization header is missing"
|
|
215
|
+
* parseBearer(""); // Throws InvalidBearerFormatError: "Authorization header must be in the format 'Bearer <token>'"
|
|
216
|
+
* parseBearer("Basic dXNlcjpwYXNz"); // Throws InvalidBearerFormatError
|
|
217
|
+
* parseBearer("Bearer"); // Throws InvalidBearerFormatError
|
|
218
|
+
* parseBearer("Bearer "); // Throws InvalidBearerFormatError
|
|
219
|
+
* ```
|
|
220
|
+
*
|
|
221
|
+
*/
|
|
222
|
+
function parseBearer(authorization: string | undefined): string {}
|
|
223
|
+
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Error Handling
|
|
227
|
+
|
|
228
|
+
Toker uses a structured error system that helps you identify and handle specific error cases. All errors extend from a base `TokerError` class.
|
|
229
|
+
|
|
230
|
+
### Error Classes Hierarchy
|
|
231
|
+
|
|
232
|
+
```
|
|
233
|
+
TokerError (abstract base class)
|
|
234
|
+
โโโ MissingAuthorizationError
|
|
235
|
+
โโโ InvalidBearerFormatError
|
|
236
|
+
โโโ InvalidTokenError
|
|
237
|
+
โโโ ExpiredTokenError
|
|
238
|
+
โโโ InactiveTokenError
|
|
239
|
+
โโโ InvalidSignatureError
|
|
240
|
+
โโโ InvalidIssuerError
|
|
241
|
+
โโโ InvalidSecretsError
|
|
242
|
+
โโโ InvalidDurationError
|
|
243
|
+
โโโ InvalidBase64Secret
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Common Properties
|
|
247
|
+
|
|
248
|
+
All error classes share these properties:
|
|
249
|
+
|
|
250
|
+
- `message`: Human-readable error description
|
|
251
|
+
- `code`: Human-readable error code (e.g., "TOKEN_EXPIRED")
|
|
252
|
+
- `statusCode`: Suggested HTTP status code (e.g., 401)
|
|
253
|
+
- `stack`: Error stack trace
|
|
254
|
+
|
|
255
|
+
### Using Error Handling
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
import { sign, verify, parseBearer, TokenExpiredError, InvalidSignatureError } from "@dwtechs/passken";
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
// Attempt to verify a token
|
|
262
|
+
const payload = verify(token, secrets);
|
|
263
|
+
// Token is valid, proceed with the payload
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (error instanceof TokenExpiredError) {
|
|
266
|
+
// Handle expired token (e.g., prompt for reauthentication)
|
|
267
|
+
console.log('Your session has expired. Please log in again.');
|
|
268
|
+
console.log(`Status code: ${error.statusCode}`); // 401
|
|
269
|
+
} else if (error instanceof InvalidSignatureError) {
|
|
270
|
+
// Handle tampered token
|
|
271
|
+
console.log('Invalid token signature detected');
|
|
272
|
+
console.log(`Status code: ${error.statusCode}`); // 401
|
|
273
|
+
} else {
|
|
274
|
+
// Handle other verification errors
|
|
275
|
+
console.log(`Token verification failed: ${error.message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Error Types and HTTP Status Codes
|
|
281
|
+
|
|
282
|
+
| Error Class | Code | Status Code | Description |
|
|
283
|
+
|-------------|------|-------------|-------------|
|
|
284
|
+
| MissingAuthorizationError | MISSING_AUTHORIZATION | 401 | Authorization header is missing |
|
|
285
|
+
| InvalidBearerFormatError | INVALID_BEARER_FORMAT | 401 | Authorization header must be in the format 'Bearer <token>' |
|
|
286
|
+
| InvalidTokenError | INVALID_TOKEN | 401 | Invalid or malformed JWT token |
|
|
287
|
+
| ExpiredTokenError | EXPIRED_TOKEN | 401 | JWT token has expired |
|
|
288
|
+
| InactiveTokenError | INACTIVE_TOKEN | 401 | JWT token cannot be used yet (nbf claim) |
|
|
289
|
+
| InvalidSignatureError | INVALID_SIGNATURE | 401 | JWT token signature is invalid |
|
|
290
|
+
| InvalidIssuerError | INVALID_ISSUER | 400 | iss must be a string or a number |
|
|
291
|
+
| InvalidSecretsError | INVALID_SECRETS | 500 | b64Keys must be an array |
|
|
292
|
+
| InvalidDurationError | INVALID_DURATION | 400 | duration must be a positive number |
|
|
293
|
+
| InvalidBase64Secret | INVALID_BASE64_SECRET | 500 | could not decode the base64 secret |
|
|
294
|
+
|
|
295
|
+
## Express.js
|
|
296
|
+
|
|
297
|
+
You can use Toker directly as Express.js middlewares using [@dwtechs/toker-express library](https://www.npmjs.com/package/@dwtechs/toker-express).
|
|
298
|
+
This way you do not have to write express controllers yourself to use **Toker**.
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
## Contributors
|
|
302
|
+
|
|
303
|
+
**Toker.js** is still in development and we would be glad to get all the help you can provide.
|
|
304
|
+
To contribute please read **[contributor.md](https://github.com/DWTechs/Toker.js/blob/main/contributor.md)** for detailed installation guide.
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
## Stack
|
|
308
|
+
|
|
309
|
+
| Purpose | Choice | Motivation |
|
|
310
|
+
| :-------------- | :------------------------------------------: | -------------------------------------------------------------: |
|
|
311
|
+
| repository | [Github](https://github.com/) | hosting for software development version control using Git |
|
|
312
|
+
| package manager | [npm](https://www.npmjs.com/get-npm) | default node.js package manager |
|
|
313
|
+
| language | [TypeScript](https://www.typescriptlang.org) | static type checking along with the latest ECMAScript features |
|
|
314
|
+
| module bundler | [Rollup.js](https://rollupjs.org) | advanced module bundler for ES6 modules |
|
|
315
|
+
| unit testing | [Jest](https://jestjs.io/) | delightful testing with a focus on simplicity |
|
package/dist/toker.d.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/*
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2022 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.js
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
export type Type = "access" | "refresh";
|
|
29
|
+
export type Header = {
|
|
30
|
+
alg: string;
|
|
31
|
+
typ: string;
|
|
32
|
+
kid: number;
|
|
33
|
+
};
|
|
34
|
+
export type Payload = {
|
|
35
|
+
iss: number | string;
|
|
36
|
+
iat: number;
|
|
37
|
+
nbf: number;
|
|
38
|
+
exp: number;
|
|
39
|
+
typ: Type;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// Error classes
|
|
43
|
+
export abstract class TokerError extends Error {
|
|
44
|
+
abstract readonly code: string;
|
|
45
|
+
abstract readonly statusCode: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class MissingAuthorizationError extends TokerError {
|
|
49
|
+
readonly code: string;
|
|
50
|
+
readonly statusCode: number;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export class InvalidBearerFormatError extends TokerError {
|
|
54
|
+
readonly code: string;
|
|
55
|
+
readonly statusCode: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class InvalidTokenError extends TokerError {
|
|
59
|
+
readonly code: string;
|
|
60
|
+
readonly statusCode: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export class ExpiredTokenError extends TokerError {
|
|
64
|
+
readonly code: string;
|
|
65
|
+
readonly statusCode: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export class InactiveTokenError extends TokerError {
|
|
69
|
+
readonly code: string;
|
|
70
|
+
readonly statusCode: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export class InvalidSignatureError extends TokerError {
|
|
74
|
+
readonly code: string;
|
|
75
|
+
readonly statusCode: number;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// export class MissingClaimsError extends TokerError {
|
|
79
|
+
// readonly code: string;
|
|
80
|
+
// readonly statusCode: number;
|
|
81
|
+
// }
|
|
82
|
+
|
|
83
|
+
export class InvalidIssuerError extends TokerError {
|
|
84
|
+
readonly code: string;
|
|
85
|
+
readonly statusCode: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export class InvalidSecretsError extends TokerError {
|
|
89
|
+
readonly code: string;
|
|
90
|
+
readonly statusCode: number;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export class InvalidDurationError extends TokerError {
|
|
94
|
+
readonly code: string;
|
|
95
|
+
readonly statusCode: number;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export class InvalidBase64Secret extends TokerError {
|
|
99
|
+
readonly code: string;
|
|
100
|
+
readonly statusCode: number;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
declare function sign(iss: number | string, duration: number, type: Type, b64Keys: string[]): string;
|
|
104
|
+
declare function verify(token: string, b64Keys: string[], ignoreExpiration?: boolean): Payload;
|
|
105
|
+
declare function parseBearer(authorization: string | undefined): string;
|
|
106
|
+
|
|
107
|
+
export {
|
|
108
|
+
sign,
|
|
109
|
+
verify,
|
|
110
|
+
parseBearer,
|
|
111
|
+
};
|
package/dist/toker.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/*
|
|
2
|
+
MIT License
|
|
3
|
+
|
|
4
|
+
Copyright (c) 2022 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.js
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { isString, isNumber, isArray, isPositive, isJson, isBase64 } from '@dwtechs/checkard';
|
|
28
|
+
import { b64Decode, b64Encode, hash, tse } from '@dwtechs/hashitaka';
|
|
29
|
+
import { Buffer } from 'buffer';
|
|
30
|
+
|
|
31
|
+
const TOKER_PREFIX = "Toker: ";
|
|
32
|
+
class TokerError extends Error {
|
|
33
|
+
constructor(message) {
|
|
34
|
+
super(message);
|
|
35
|
+
this.name = this.constructor.name;
|
|
36
|
+
if (Error.captureStackTrace) {
|
|
37
|
+
Error.captureStackTrace(this, this.constructor);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
class MissingAuthorizationError extends TokerError {
|
|
42
|
+
constructor(message = `${TOKER_PREFIX}Authorization header is missing`) {
|
|
43
|
+
super(message);
|
|
44
|
+
this.code = "MISSING_AUTHORIZATION";
|
|
45
|
+
this.statusCode = 401;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
class InvalidBearerFormatError extends TokerError {
|
|
49
|
+
constructor(message = `${TOKER_PREFIX}Authorization header must be in the format 'Bearer <token>'`) {
|
|
50
|
+
super(message);
|
|
51
|
+
this.code = "INVALID_BEARER_FORMAT";
|
|
52
|
+
this.statusCode = 401;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
class InvalidTokenError extends TokerError {
|
|
56
|
+
constructor(message = `${TOKER_PREFIX}Invalid or malformed JWT token`) {
|
|
57
|
+
super(message);
|
|
58
|
+
this.code = "INVALID_TOKEN";
|
|
59
|
+
this.statusCode = 401;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
class ExpiredTokenError extends TokerError {
|
|
63
|
+
constructor(message = `${TOKER_PREFIX}JWT token has expired`) {
|
|
64
|
+
super(message);
|
|
65
|
+
this.code = "EXPIRED_TOKEN";
|
|
66
|
+
this.statusCode = 401;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
class InactiveTokenError extends TokerError {
|
|
70
|
+
constructor(message = `${TOKER_PREFIX}JWT token cannot be used yet (nbf claim)`) {
|
|
71
|
+
super(message);
|
|
72
|
+
this.code = "INACTIVE_TOKEN";
|
|
73
|
+
this.statusCode = 401;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
class InvalidSignatureError extends TokerError {
|
|
77
|
+
constructor(message = `${TOKER_PREFIX}JWT token signature is invalid`) {
|
|
78
|
+
super(message);
|
|
79
|
+
this.code = "INVALID_SIGNATURE";
|
|
80
|
+
this.statusCode = 401;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
class InvalidIssuerError extends TokerError {
|
|
84
|
+
constructor(message = `${TOKER_PREFIX}iss must be a string or a number`) {
|
|
85
|
+
super(message);
|
|
86
|
+
this.code = "INVALID_ISSUER";
|
|
87
|
+
this.statusCode = 400;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
class InvalidSecretsError extends TokerError {
|
|
91
|
+
constructor(message = `${TOKER_PREFIX}b64Keys must be an array`) {
|
|
92
|
+
super(message);
|
|
93
|
+
this.code = "INVALID_SECRETS";
|
|
94
|
+
this.statusCode = 500;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
class InvalidDurationError extends TokerError {
|
|
98
|
+
constructor(message = `${TOKER_PREFIX}duration must be a positive number`) {
|
|
99
|
+
super(message);
|
|
100
|
+
this.code = "INVALID_DURATION";
|
|
101
|
+
this.statusCode = 400;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
class InvalidBase64Secret extends TokerError {
|
|
105
|
+
constructor(message = `${TOKER_PREFIX}could not decode the base64 secret`) {
|
|
106
|
+
super(message);
|
|
107
|
+
this.code = "INVALID_BASE64_SECRET";
|
|
108
|
+
this.statusCode = 500;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const header = {
|
|
113
|
+
alg: "HS256",
|
|
114
|
+
typ: "JWT",
|
|
115
|
+
kid: 0,
|
|
116
|
+
};
|
|
117
|
+
function sign(iss, duration, type, b64Keys) {
|
|
118
|
+
if (!isString(iss, "!0") && !isNumber(iss, true))
|
|
119
|
+
throw new InvalidIssuerError();
|
|
120
|
+
if (!isArray(b64Keys, ">", 0))
|
|
121
|
+
throw new InvalidSecretsError();
|
|
122
|
+
if (!isNumber(duration, false) || !isPositive(duration, true))
|
|
123
|
+
throw new InvalidDurationError();
|
|
124
|
+
header.kid = randomPick(b64Keys);
|
|
125
|
+
const b64Secret = b64Keys[header.kid];
|
|
126
|
+
const secret = b64Decode(b64Secret, true);
|
|
127
|
+
if (!secret)
|
|
128
|
+
throw new InvalidBase64Secret();
|
|
129
|
+
const iat = Math.floor(Date.now() / 1000);
|
|
130
|
+
const nbf = iat + 1;
|
|
131
|
+
const exp = duration > 60 ? iat + duration : iat + 60 * 15;
|
|
132
|
+
const typ = type === "refresh" ? type : "access";
|
|
133
|
+
const payload = { iss, iat, nbf, exp, typ };
|
|
134
|
+
const b64Header = b64Encode(JSON.stringify(header));
|
|
135
|
+
const b64Payload = b64Encode(JSON.stringify(payload));
|
|
136
|
+
const b64Signature = hash(`${b64Header}.${b64Payload}`, secret);
|
|
137
|
+
return `${b64Header}.${b64Payload}.${b64Signature}`;
|
|
138
|
+
}
|
|
139
|
+
function verify(token, b64Keys, ignoreExpiration = false) {
|
|
140
|
+
const segments = token.split(".");
|
|
141
|
+
if (segments.length !== 3)
|
|
142
|
+
throw new InvalidTokenError();
|
|
143
|
+
const [b64Header, b64Payload, b64Signature] = segments;
|
|
144
|
+
if (!b64Header || !b64Payload || !b64Signature)
|
|
145
|
+
throw new InvalidTokenError();
|
|
146
|
+
if (!isArray(b64Keys, ">", 0))
|
|
147
|
+
throw new InvalidSecretsError();
|
|
148
|
+
const headerString = b64Decode(b64Header);
|
|
149
|
+
const payloadString = b64Decode(b64Payload);
|
|
150
|
+
if (!isJson(headerString) || !isJson(payloadString))
|
|
151
|
+
throw new InvalidTokenError();
|
|
152
|
+
const header = JSON.parse(headerString);
|
|
153
|
+
const payload = JSON.parse(payloadString);
|
|
154
|
+
if (header.alg !== "HS256")
|
|
155
|
+
throw new InvalidTokenError();
|
|
156
|
+
if (header.typ !== "JWT")
|
|
157
|
+
throw new InvalidTokenError();
|
|
158
|
+
if (!isString(header.kid, "!0") && !isNumber(header.kid, true))
|
|
159
|
+
throw new InvalidTokenError();
|
|
160
|
+
const now = Math.floor(Date.now() / 1000);
|
|
161
|
+
if (payload.nbf && payload.nbf > now)
|
|
162
|
+
throw new InactiveTokenError();
|
|
163
|
+
if (!ignoreExpiration && payload.exp < now)
|
|
164
|
+
throw new ExpiredTokenError();
|
|
165
|
+
const b64Secret = b64Keys[header.kid];
|
|
166
|
+
if (!isBase64(b64Secret, true))
|
|
167
|
+
throw new InvalidBase64Secret();
|
|
168
|
+
const secret = b64Decode(b64Secret);
|
|
169
|
+
const expectedSignature = hash(`${b64Header}.${b64Payload}`, secret);
|
|
170
|
+
const safeA = Buffer.from(expectedSignature);
|
|
171
|
+
const safeB = Buffer.from(b64Signature);
|
|
172
|
+
if (!tse(safeA, safeB))
|
|
173
|
+
throw new InvalidSignatureError();
|
|
174
|
+
return payload;
|
|
175
|
+
}
|
|
176
|
+
function parseBearer(authorization) {
|
|
177
|
+
if (!authorization)
|
|
178
|
+
throw new MissingAuthorizationError();
|
|
179
|
+
if (!authorization.startsWith("Bearer "))
|
|
180
|
+
throw new InvalidBearerFormatError();
|
|
181
|
+
const parts = authorization.split(" ").filter(part => part.length > 0);
|
|
182
|
+
if (parts.length < 2 || !parts[1])
|
|
183
|
+
throw new InvalidBearerFormatError();
|
|
184
|
+
return parts[1];
|
|
185
|
+
}
|
|
186
|
+
function randomPick(array) {
|
|
187
|
+
return Math.floor(Math.random() * array.length);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export { ExpiredTokenError, InactiveTokenError, InvalidBase64Secret, InvalidBearerFormatError, InvalidDurationError, InvalidIssuerError, InvalidSecretsError, InvalidSignatureError, InvalidTokenError, MissingAuthorizationError, TokerError, parseBearer, sign, verify };
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@dwtechs/toker",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Open source JWT management library for Node.js to sign, verify and parse bearer safely.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"jwt",
|
|
7
|
+
"token",
|
|
8
|
+
"bearer",
|
|
9
|
+
"authentication"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://github.com/DWTechs/Toker.js",
|
|
12
|
+
"main": "dist/toker",
|
|
13
|
+
"types": "dist/toker",
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/DWTechs/Toker.js"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "https://github.com/DWTechs/Toker.js/issues",
|
|
20
|
+
"email": ""
|
|
21
|
+
},
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"author": {
|
|
24
|
+
"name": "Ludovic Cluber",
|
|
25
|
+
"email": "http://www.lcluber.com/contact",
|
|
26
|
+
"url": "http://www.lcluber.com"
|
|
27
|
+
},
|
|
28
|
+
"contributors": [],
|
|
29
|
+
"scripts": {
|
|
30
|
+
"start": "",
|
|
31
|
+
"prebuild": "npm install",
|
|
32
|
+
"build": "node ./scripts/clear && tsc && npm run rollup && node ./scripts/copy && npm run test",
|
|
33
|
+
"rollup": "rollup --config rollup.config.mjs",
|
|
34
|
+
"test": "jest --coverage"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"dist/"
|
|
38
|
+
],
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@dwtechs/checkard": "3.2.3",
|
|
41
|
+
"@dwtechs/hashitaka": "0.2.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@babel/preset-env": "7.26.0",
|
|
45
|
+
"@types/node": "24.1.0",
|
|
46
|
+
"jest": "29.7.0",
|
|
47
|
+
"rollup": "4.24.0",
|
|
48
|
+
"typescript": "5.6.3"
|
|
49
|
+
}
|
|
50
|
+
}
|