@dwtechs/toker 0.1.0 โ 0.1.2
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 +2 -17
- package/dist/toker.d.ts +44 -50
- package/dist/toker.js +80 -49
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
[](https://opensource.org/licenses/MIT)
|
|
3
3
|
[](https://www.npmjs.com/package/@dwtechs/toker)
|
|
4
4
|
[](https://www.npmjs.com/package/@dwtechs/toker)
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
- [Synopsis](#synopsis)
|
|
8
8
|
- [Support](#support)
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
|
|
21
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
22
|
|
|
23
|
-
- ๐ฆ Only
|
|
23
|
+
- ๐ฆ Only 2 dependencies to check inputs variables and secure hash creation and comparison
|
|
24
24
|
- ๐ชถ Very lightweight
|
|
25
25
|
- ๐งช Thoroughly tested
|
|
26
26
|
- ๐ Shipped as EcmaScrypt module
|
|
@@ -227,21 +227,6 @@ function parseBearer(authorization: string | undefined): string {}
|
|
|
227
227
|
|
|
228
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
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
230
|
|
|
246
231
|
### Common Properties
|
|
247
232
|
|
package/dist/toker.d.ts
CHANGED
|
@@ -38,66 +38,60 @@ export type Payload = {
|
|
|
38
38
|
exp: number;
|
|
39
39
|
typ: Type;
|
|
40
40
|
};
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
abstract readonly statusCode: number;
|
|
41
|
+
export declare abstract class TokerError extends Error {
|
|
42
|
+
abstract readonly code: string;
|
|
43
|
+
abstract readonly statusCode: number;
|
|
44
|
+
constructor(message: string, causedBy?: Error);
|
|
46
45
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
46
|
+
export declare class MissingAuthorizationError extends TokerError {
|
|
47
|
+
readonly code = "MISSING_AUTHORIZATION";
|
|
48
|
+
readonly statusCode = 401;
|
|
49
|
+
constructor();
|
|
51
50
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
51
|
+
export declare class InvalidBearerFormatError extends TokerError {
|
|
52
|
+
readonly code = "INVALID_BEARER_FORMAT";
|
|
53
|
+
readonly statusCode = 401;
|
|
54
|
+
constructor();
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
export declare class InvalidTokenError extends TokerError {
|
|
57
|
+
readonly code = "INVALID_TOKEN";
|
|
58
|
+
readonly statusCode = 401;
|
|
59
|
+
constructor(causedBy?: Error);
|
|
61
60
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
export declare class ExpiredTokenError extends TokerError {
|
|
62
|
+
readonly code = "EXPIRED_TOKEN";
|
|
63
|
+
readonly statusCode = 401;
|
|
64
|
+
constructor();
|
|
66
65
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
66
|
+
export declare class InactiveTokenError extends TokerError {
|
|
67
|
+
readonly code = "INACTIVE_TOKEN";
|
|
68
|
+
readonly statusCode = 401;
|
|
69
|
+
constructor();
|
|
71
70
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
71
|
+
export declare class InvalidSignatureError extends TokerError {
|
|
72
|
+
readonly code = "INVALID_SIGNATURE";
|
|
73
|
+
readonly statusCode = 401;
|
|
74
|
+
constructor(causedBy?: Error);
|
|
76
75
|
}
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
// }
|
|
82
|
-
|
|
83
|
-
export class InvalidIssuerError extends TokerError {
|
|
84
|
-
readonly code: string;
|
|
85
|
-
readonly statusCode: number;
|
|
76
|
+
export declare class InvalidIssuerError extends TokerError {
|
|
77
|
+
readonly code = "INVALID_ISSUER";
|
|
78
|
+
readonly statusCode = 400;
|
|
79
|
+
constructor();
|
|
86
80
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
81
|
+
export declare class InvalidSecretsError extends TokerError {
|
|
82
|
+
readonly code = "INVALID_SECRETS";
|
|
83
|
+
readonly statusCode = 500;
|
|
84
|
+
constructor(causedBy?: Error);
|
|
91
85
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
86
|
+
export declare class InvalidDurationError extends TokerError {
|
|
87
|
+
readonly code = "INVALID_DURATION";
|
|
88
|
+
readonly statusCode = 400;
|
|
89
|
+
constructor(causedBy?: Error);
|
|
96
90
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
91
|
+
export declare class InvalidBase64Secret extends TokerError {
|
|
92
|
+
readonly code = "INVALID_BASE64_SECRET";
|
|
93
|
+
readonly statusCode = 500;
|
|
94
|
+
constructor(causedBy?: Error);
|
|
101
95
|
}
|
|
102
96
|
|
|
103
97
|
declare function sign(iss: number | string, duration: number, type: Type, b64Keys: string[]): string;
|
package/dist/toker.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/*
|
|
2
2
|
MIT License
|
|
3
3
|
|
|
4
4
|
Copyright (c) 2022 DWTechs
|
|
@@ -20,90 +20,93 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
|
20
20
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
21
21
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
22
22
|
SOFTWARE.
|
|
23
|
-
|
|
24
|
-
https://github.com/DWTechs/Toker.js
|
|
25
|
-
*/
|
|
26
|
-
|
|
23
|
+
|
|
24
|
+
https://github.com/DWTechs/Toker.js
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
27
|
import { isString, isNumber, isArray, isPositive, isJson, isBase64 } from '@dwtechs/checkard';
|
|
28
28
|
import { b64Decode, b64Encode, hash, tse } from '@dwtechs/hashitaka';
|
|
29
29
|
import { Buffer } from 'buffer';
|
|
30
30
|
|
|
31
31
|
const TOKER_PREFIX = "Toker: ";
|
|
32
|
+
function chainMessage(message, err) {
|
|
33
|
+
return `${message} - caused by: ${err.message}`;
|
|
34
|
+
}
|
|
32
35
|
class TokerError extends Error {
|
|
33
|
-
constructor(message) {
|
|
34
|
-
super(message);
|
|
35
|
-
this.name = this.constructor.name
|
|
36
|
+
constructor(message, causedBy) {
|
|
37
|
+
super(causedBy ? chainMessage(message, causedBy) : message);
|
|
38
|
+
this.name = `${TOKER_PREFIX}${this.constructor.name}`;
|
|
36
39
|
if (Error.captureStackTrace) {
|
|
37
40
|
Error.captureStackTrace(this, this.constructor);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
class MissingAuthorizationError extends TokerError {
|
|
42
|
-
constructor(
|
|
43
|
-
super(
|
|
45
|
+
constructor() {
|
|
46
|
+
super(`${TOKER_PREFIX}Authorization header is missing`);
|
|
44
47
|
this.code = "MISSING_AUTHORIZATION";
|
|
45
48
|
this.statusCode = 401;
|
|
46
49
|
}
|
|
47
50
|
}
|
|
48
51
|
class InvalidBearerFormatError extends TokerError {
|
|
49
|
-
constructor(
|
|
50
|
-
super(
|
|
52
|
+
constructor() {
|
|
53
|
+
super(`${TOKER_PREFIX}Authorization header must be in the format 'Bearer <token>'`);
|
|
51
54
|
this.code = "INVALID_BEARER_FORMAT";
|
|
52
55
|
this.statusCode = 401;
|
|
53
56
|
}
|
|
54
57
|
}
|
|
55
58
|
class InvalidTokenError extends TokerError {
|
|
56
|
-
constructor(
|
|
57
|
-
super(
|
|
59
|
+
constructor(causedBy) {
|
|
60
|
+
super(`${TOKER_PREFIX}Invalid or malformed JWT token`, causedBy);
|
|
58
61
|
this.code = "INVALID_TOKEN";
|
|
59
62
|
this.statusCode = 401;
|
|
60
63
|
}
|
|
61
64
|
}
|
|
62
65
|
class ExpiredTokenError extends TokerError {
|
|
63
|
-
constructor(
|
|
64
|
-
super(
|
|
66
|
+
constructor() {
|
|
67
|
+
super(`${TOKER_PREFIX}JWT token has expired`);
|
|
65
68
|
this.code = "EXPIRED_TOKEN";
|
|
66
69
|
this.statusCode = 401;
|
|
67
70
|
}
|
|
68
71
|
}
|
|
69
72
|
class InactiveTokenError extends TokerError {
|
|
70
|
-
constructor(
|
|
71
|
-
super(
|
|
73
|
+
constructor() {
|
|
74
|
+
super(`${TOKER_PREFIX}JWT token cannot be used yet (nbf claim)`);
|
|
72
75
|
this.code = "INACTIVE_TOKEN";
|
|
73
76
|
this.statusCode = 401;
|
|
74
77
|
}
|
|
75
78
|
}
|
|
76
79
|
class InvalidSignatureError extends TokerError {
|
|
77
|
-
constructor(
|
|
78
|
-
super(
|
|
80
|
+
constructor(causedBy) {
|
|
81
|
+
super(`${TOKER_PREFIX}JWT token signature is invalid`, causedBy);
|
|
79
82
|
this.code = "INVALID_SIGNATURE";
|
|
80
83
|
this.statusCode = 401;
|
|
81
84
|
}
|
|
82
85
|
}
|
|
83
86
|
class InvalidIssuerError extends TokerError {
|
|
84
|
-
constructor(
|
|
85
|
-
super(
|
|
87
|
+
constructor() {
|
|
88
|
+
super(`${TOKER_PREFIX}iss must be a string or a number`);
|
|
86
89
|
this.code = "INVALID_ISSUER";
|
|
87
90
|
this.statusCode = 400;
|
|
88
91
|
}
|
|
89
92
|
}
|
|
90
93
|
class InvalidSecretsError extends TokerError {
|
|
91
|
-
constructor(
|
|
92
|
-
super(
|
|
94
|
+
constructor(causedBy) {
|
|
95
|
+
super(`${TOKER_PREFIX}b64Keys must be an array`, causedBy);
|
|
93
96
|
this.code = "INVALID_SECRETS";
|
|
94
97
|
this.statusCode = 500;
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
class InvalidDurationError extends TokerError {
|
|
98
|
-
constructor(
|
|
99
|
-
super(
|
|
101
|
+
constructor(causedBy) {
|
|
102
|
+
super(`${TOKER_PREFIX}duration must be a positive number`, causedBy);
|
|
100
103
|
this.code = "INVALID_DURATION";
|
|
101
104
|
this.statusCode = 400;
|
|
102
105
|
}
|
|
103
106
|
}
|
|
104
107
|
class InvalidBase64Secret extends TokerError {
|
|
105
|
-
constructor(
|
|
106
|
-
super(
|
|
108
|
+
constructor(causedBy) {
|
|
109
|
+
super(`${TOKER_PREFIX}could not decode the base64 secret`, causedBy);
|
|
107
110
|
this.code = "INVALID_BASE64_SECRET";
|
|
108
111
|
this.statusCode = 500;
|
|
109
112
|
}
|
|
@@ -117,22 +120,28 @@ const header = {
|
|
|
117
120
|
function sign(iss, duration, type, b64Keys) {
|
|
118
121
|
if (!isString(iss, "!0") && !isNumber(iss, true))
|
|
119
122
|
throw new InvalidIssuerError();
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
try {
|
|
124
|
+
isArray(b64Keys, ">", 0, true);
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
throw new InvalidSecretsError(err);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
isPositive(duration, true, true);
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
throw new InvalidDurationError(err);
|
|
134
|
+
}
|
|
124
135
|
header.kid = randomPick(b64Keys);
|
|
125
136
|
const b64Secret = b64Keys[header.kid];
|
|
126
137
|
const secret = b64Decode(b64Secret, true);
|
|
127
|
-
if (!secret)
|
|
128
|
-
throw new InvalidBase64Secret();
|
|
129
138
|
const iat = Math.floor(Date.now() / 1000);
|
|
130
|
-
const nbf = iat
|
|
139
|
+
const nbf = iat;
|
|
131
140
|
const exp = duration > 60 ? iat + duration : iat + 60 * 15;
|
|
132
141
|
const typ = type === "refresh" ? type : "access";
|
|
133
142
|
const payload = { iss, iat, nbf, exp, typ };
|
|
134
|
-
const b64Header = b64Encode(JSON.stringify(header));
|
|
135
|
-
const b64Payload = b64Encode(JSON.stringify(payload));
|
|
143
|
+
const b64Header = b64Encode(JSON.stringify(header), true);
|
|
144
|
+
const b64Payload = b64Encode(JSON.stringify(payload), true);
|
|
136
145
|
const b64Signature = hash(`${b64Header}.${b64Payload}`, secret);
|
|
137
146
|
return `${b64Header}.${b64Payload}.${b64Signature}`;
|
|
138
147
|
}
|
|
@@ -143,14 +152,25 @@ function verify(token, b64Keys, ignoreExpiration = false) {
|
|
|
143
152
|
const [b64Header, b64Payload, b64Signature] = segments;
|
|
144
153
|
if (!b64Header || !b64Payload || !b64Signature)
|
|
145
154
|
throw new InvalidTokenError();
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
155
|
+
try {
|
|
156
|
+
isArray(b64Keys, ">", 0, true);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
throw new InvalidSecretsError(err);
|
|
160
|
+
}
|
|
161
|
+
let headerStr;
|
|
162
|
+
let payloadStr;
|
|
163
|
+
try {
|
|
164
|
+
headerStr = b64Decode(b64Header, true);
|
|
165
|
+
payloadStr = b64Decode(b64Payload, true);
|
|
166
|
+
isJson(headerStr, true);
|
|
167
|
+
isJson(payloadStr, true);
|
|
168
|
+
}
|
|
169
|
+
catch (err) {
|
|
170
|
+
throw new InvalidTokenError(err);
|
|
171
|
+
}
|
|
172
|
+
const header = JSON.parse(headerStr);
|
|
173
|
+
const payload = JSON.parse(payloadStr);
|
|
154
174
|
if (header.alg !== "HS256")
|
|
155
175
|
throw new InvalidTokenError();
|
|
156
176
|
if (header.typ !== "JWT")
|
|
@@ -163,13 +183,24 @@ function verify(token, b64Keys, ignoreExpiration = false) {
|
|
|
163
183
|
if (!ignoreExpiration && payload.exp < now)
|
|
164
184
|
throw new ExpiredTokenError();
|
|
165
185
|
const b64Secret = b64Keys[header.kid];
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
186
|
+
try {
|
|
187
|
+
isBase64(b64Secret, true, true);
|
|
188
|
+
}
|
|
189
|
+
catch (err) {
|
|
190
|
+
throw new InvalidBase64Secret(err);
|
|
191
|
+
}
|
|
192
|
+
const secret = b64Decode(b64Secret, true);
|
|
169
193
|
const expectedSignature = hash(`${b64Header}.${b64Payload}`, secret);
|
|
170
194
|
const safeA = Buffer.from(expectedSignature);
|
|
171
195
|
const safeB = Buffer.from(b64Signature);
|
|
172
|
-
|
|
196
|
+
let signaturesMatch;
|
|
197
|
+
try {
|
|
198
|
+
signaturesMatch = tse(safeA, safeB);
|
|
199
|
+
}
|
|
200
|
+
catch (err) {
|
|
201
|
+
throw new InvalidSignatureError(err);
|
|
202
|
+
}
|
|
203
|
+
if (!signaturesMatch)
|
|
173
204
|
throw new InvalidSignatureError();
|
|
174
205
|
return payload;
|
|
175
206
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dwtechs/toker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Open source JWT management library for Node.js to sign, verify and parse bearer safely.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"jwt",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dist/"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@dwtechs/checkard": "3.
|
|
41
|
-
"@dwtechs/hashitaka": "0.
|
|
40
|
+
"@dwtechs/checkard": "3.5.1",
|
|
41
|
+
"@dwtechs/hashitaka": "0.3.1"
|
|
42
42
|
},
|
|
43
43
|
"devDependencies": {
|
|
44
44
|
"@babel/preset-env": "7.26.0",
|