eslint-plugin-jwt-security 2.3.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/AGENTS.md +102 -0
- package/CHANGELOG.md +161 -0
- package/LICENSE +23 -0
- package/README.md +158 -0
- package/dist/AGENTS.md +102 -0
- package/dist/CHANGELOG.md +161 -0
- package/dist/LICENSE +23 -0
- package/dist/README.md +158 -0
- package/dist/package.json +125 -0
- package/dist/src/index.d.ts +101 -0
- package/dist/src/index.js +171 -0
- package/dist/src/oxlint.d.ts +20 -0
- package/dist/src/oxlint.js +21 -0
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
- package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-none/index.js +222 -0
- package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
- package/dist/src/rules/no-decode-without-verify/index.js +131 -0
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
- package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
- package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
- package/dist/src/rules/no-sensitive-payload/index.js +103 -0
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
- package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
- package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
- package/dist/src/rules/no-weak-secret/index.js +165 -0
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
- package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
- package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
- package/dist/src/rules/require-audience-validation/index.js +100 -0
- package/dist/src/rules/require-expiration/index.d.ts +23 -0
- package/dist/src/rules/require-expiration/index.js +136 -0
- package/dist/src/rules/require-issued-at/index.d.ts +23 -0
- package/dist/src/rules/require-issued-at/index.js +113 -0
- package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
- package/dist/src/rules/require-issuer-validation/index.js +100 -0
- package/dist/src/rules/require-max-age/index.d.ts +24 -0
- package/dist/src/rules/require-max-age/index.js +93 -0
- package/dist/src/types/index.d.ts +82 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/utils/index.d.ts +98 -0
- package/dist/src/utils/index.js +238 -0
- package/package.json +125 -0
- package/src/__compatibility__/jwt-interface.spec.ts +192 -0
- package/src/index.test.ts +47 -0
- package/src/index.ts +209 -0
- package/src/oxlint.test.ts +61 -0
- package/src/oxlint.ts +23 -0
- package/src/rules/no-algorithm-confusion/index.ts +208 -0
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
- package/src/rules/no-algorithm-none/index.ts +270 -0
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
- package/src/rules/no-decode-without-verify/index.ts +162 -0
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
- package/src/rules/no-hardcoded-secret/index.ts +230 -0
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
- package/src/rules/no-sensitive-payload/index.ts +123 -0
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
- package/src/rules/no-timestamp-manipulation/index.ts +118 -0
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
- package/src/rules/no-weak-secret/index.ts +204 -0
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
- package/src/rules/require-algorithm-whitelist/index.ts +145 -0
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
- package/src/rules/require-audience-validation/index.ts +120 -0
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
- package/src/rules/require-expiration/index.ts +170 -0
- package/src/rules/require-expiration/require-expiration.test.ts +135 -0
- package/src/rules/require-issued-at/index.ts +143 -0
- package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
- package/src/rules/require-issuer-validation/index.ts +120 -0
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
- package/src/rules/require-max-age/index.ts +117 -0
- package/src/rules/require-max-age/require-max-age.test.ts +69 -0
- package/src/tests/coverage-dual-layer.test.ts +323 -0
- package/src/types/index.ts +107 -0
- package/src/utils/index.ts +285 -0
- package/src/utils/utils.test.ts +324 -0
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.JWT_METHODS = exports.SENSITIVE_PAYLOAD_FIELDS = exports.SECURE_ALGORITHMS = exports.SYMMETRIC_ALGORITHMS = exports.INSECURE_ALGORITHMS = exports.JWT_LIBRARIES = void 0;
|
|
9
|
+
exports.isJwtLibraryCall = isJwtLibraryCall;
|
|
10
|
+
exports.isStringLiteral = isStringLiteral;
|
|
11
|
+
exports.extractAlgorithms = extractAlgorithms;
|
|
12
|
+
exports.hasOption = hasOption;
|
|
13
|
+
exports.getOptionValue = getOptionValue;
|
|
14
|
+
exports.isWeakSecret = isWeakSecret;
|
|
15
|
+
exports.isEnvVariable = isEnvVariable;
|
|
16
|
+
exports.isSignOperation = isSignOperation;
|
|
17
|
+
exports.isVerifyOperation = isVerifyOperation;
|
|
18
|
+
exports.isDecodeOperation = isDecodeOperation;
|
|
19
|
+
exports.getOptionsArgument = getOptionsArgument;
|
|
20
|
+
exports.isTestFile = isTestFile;
|
|
21
|
+
/**
|
|
22
|
+
* Supported JWT libraries
|
|
23
|
+
*/
|
|
24
|
+
exports.JWT_LIBRARIES = {
|
|
25
|
+
JSONWEBTOKEN: 'jsonwebtoken',
|
|
26
|
+
JOSE: 'jose',
|
|
27
|
+
EXPRESS_JWT: 'express-jwt',
|
|
28
|
+
NESTJS_JWT: '@nestjs/jwt',
|
|
29
|
+
JWKS_RSA: 'jwks-rsa',
|
|
30
|
+
JWT_DECODE: 'jwt-decode',
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Insecure algorithms that should be flagged
|
|
34
|
+
*/
|
|
35
|
+
exports.INSECURE_ALGORITHMS = new Set([
|
|
36
|
+
'none',
|
|
37
|
+
'None',
|
|
38
|
+
'NONE',
|
|
39
|
+
'HS256', // Only insecure when used with public keys
|
|
40
|
+
'HS384',
|
|
41
|
+
'HS512',
|
|
42
|
+
]);
|
|
43
|
+
/**
|
|
44
|
+
* Algorithms vulnerable to confusion attacks when used with asymmetric keys
|
|
45
|
+
*/
|
|
46
|
+
exports.SYMMETRIC_ALGORITHMS = new Set(['HS256', 'HS384', 'HS512']);
|
|
47
|
+
/**
|
|
48
|
+
* Recommended secure algorithms
|
|
49
|
+
*/
|
|
50
|
+
exports.SECURE_ALGORITHMS = new Set([
|
|
51
|
+
'RS256',
|
|
52
|
+
'RS384',
|
|
53
|
+
'RS512',
|
|
54
|
+
'ES256',
|
|
55
|
+
'ES384',
|
|
56
|
+
'ES512',
|
|
57
|
+
'PS256',
|
|
58
|
+
'PS384',
|
|
59
|
+
'PS512',
|
|
60
|
+
'EdDSA',
|
|
61
|
+
]);
|
|
62
|
+
/**
|
|
63
|
+
* Sensitive field names that should not be in JWT payload
|
|
64
|
+
* All lowercase for case-insensitive matching
|
|
65
|
+
*/
|
|
66
|
+
exports.SENSITIVE_PAYLOAD_FIELDS = new Set([
|
|
67
|
+
// Passwords
|
|
68
|
+
'password', 'passwd', 'pwd', 'pass', 'secret',
|
|
69
|
+
// API Keys
|
|
70
|
+
'apikey', 'api_key', 'api-key',
|
|
71
|
+
'apisecret', 'api_secret', 'api-secret',
|
|
72
|
+
// Tokens
|
|
73
|
+
'accesstoken', 'access_token', 'access-token',
|
|
74
|
+
'refreshtoken', 'refresh_token', 'refresh-token',
|
|
75
|
+
'bearertoken', 'bearer_token', 'bearer-token',
|
|
76
|
+
// Keys
|
|
77
|
+
'privatekey', 'private_key', 'private-key',
|
|
78
|
+
'secretkey', 'secret_key', 'secret-key',
|
|
79
|
+
// PII - Personal Identifiable Information
|
|
80
|
+
'email', 'emailaddress', 'email_address', 'email-address',
|
|
81
|
+
'phone', 'phonenumber', 'phone_number', 'phone-number',
|
|
82
|
+
'ssn', 'socialsecuritynumber', 'social_security_number',
|
|
83
|
+
'dob', 'dateofbirth', 'date_of_birth', 'birthdate',
|
|
84
|
+
'address', 'streetaddress', 'street_address',
|
|
85
|
+
// Financial
|
|
86
|
+
'creditcard', 'credit_card', 'credit-card',
|
|
87
|
+
'cardnumber', 'card_number', 'card-number',
|
|
88
|
+
'cvv', 'cvc', 'securitycode', 'security_code',
|
|
89
|
+
'pin', 'pincode', 'pin_code',
|
|
90
|
+
'bankaccount', 'bank_account', 'bank-account',
|
|
91
|
+
'accountnumber', 'account_number', 'account-number',
|
|
92
|
+
'routingnumber', 'routing_number', 'routing-number',
|
|
93
|
+
]);
|
|
94
|
+
/**
|
|
95
|
+
* JWT method patterns for different operations
|
|
96
|
+
*/
|
|
97
|
+
exports.JWT_METHODS = {
|
|
98
|
+
SIGN: new Set(['sign', 'signJWT', 'SignJWT']),
|
|
99
|
+
VERIFY: new Set(['verify', 'verifyJWT', 'jwtVerify']),
|
|
100
|
+
DECODE: new Set(['decode', 'jwtDecode', 'decodeJWT']),
|
|
101
|
+
};
|
|
102
|
+
/**
|
|
103
|
+
* Check if a node represents a call to a JWT library method
|
|
104
|
+
*/
|
|
105
|
+
function isJwtLibraryCall(node, targetMethods) {
|
|
106
|
+
// Check member expression: jwt.verify(), jose.jwtVerify()
|
|
107
|
+
if (node.callee.type === 'MemberExpression') {
|
|
108
|
+
const property = node.callee.property;
|
|
109
|
+
if (property.type === 'Identifier') {
|
|
110
|
+
return targetMethods.has(property.name);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Check direct calls: jwtVerify(), jwtDecode()
|
|
114
|
+
if (node.callee.type === 'Identifier') {
|
|
115
|
+
return targetMethods.has(node.callee.name);
|
|
116
|
+
}
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Check if a node is a string literal with a specific value
|
|
121
|
+
*/
|
|
122
|
+
function isStringLiteral(node, value) {
|
|
123
|
+
if (node.type !== 'Literal' || typeof node.value !== 'string') {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return value === undefined || node.value === value;
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Extract algorithm from options object
|
|
130
|
+
*/
|
|
131
|
+
function extractAlgorithms(optionsNode) {
|
|
132
|
+
const algorithms = [];
|
|
133
|
+
for (const prop of optionsNode.properties) {
|
|
134
|
+
if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const keyName = prop.key.name;
|
|
138
|
+
if (keyName !== 'algorithms' && keyName !== 'algorithm' && keyName !== 'alg') {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Single algorithm: { algorithm: 'HS256' }
|
|
142
|
+
if (prop.value.type === 'Literal' && typeof prop.value.value === 'string') {
|
|
143
|
+
algorithms.push(prop.value.value);
|
|
144
|
+
}
|
|
145
|
+
// Array of algorithms: { algorithms: ['RS256', 'ES256'] }
|
|
146
|
+
if (prop.value.type === 'ArrayExpression') {
|
|
147
|
+
for (const elem of prop.value.elements) {
|
|
148
|
+
if (elem && elem.type === 'Literal' && typeof elem.value === 'string') {
|
|
149
|
+
algorithms.push(elem.value);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return algorithms;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Check if options object has a specific property set
|
|
158
|
+
*/
|
|
159
|
+
function hasOption(optionsNode, optionName) {
|
|
160
|
+
return optionsNode.properties.some((prop) => prop.type === 'Property' &&
|
|
161
|
+
prop.key.type === 'Identifier' &&
|
|
162
|
+
prop.key.name === optionName);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Get the value of a specific option from options object
|
|
166
|
+
*/
|
|
167
|
+
function getOptionValue(optionsNode, optionName) {
|
|
168
|
+
for (const prop of optionsNode.properties) {
|
|
169
|
+
if (prop.type === 'Property' &&
|
|
170
|
+
prop.key.type === 'Identifier' &&
|
|
171
|
+
prop.key.name === optionName) {
|
|
172
|
+
return prop.value;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return undefined;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Check if a literal appears to be a weak secret (short string)
|
|
179
|
+
*/
|
|
180
|
+
function isWeakSecret(node, minLength = 32) {
|
|
181
|
+
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
182
|
+
return node.value.length < minLength;
|
|
183
|
+
}
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Check if a node is an environment variable access (safe pattern)
|
|
188
|
+
*/
|
|
189
|
+
function isEnvVariable(node) {
|
|
190
|
+
// process.env.JWT_SECRET
|
|
191
|
+
if (node.type === 'MemberExpression' &&
|
|
192
|
+
node.object.type === 'MemberExpression' &&
|
|
193
|
+
node.object.object.type === 'Identifier' &&
|
|
194
|
+
node.object.object.name === 'process' &&
|
|
195
|
+
node.object.property.type === 'Identifier' &&
|
|
196
|
+
node.object.property.name === 'env') {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Check if this call looks like a JWT sign operation
|
|
203
|
+
*/
|
|
204
|
+
function isSignOperation(node) {
|
|
205
|
+
return isJwtLibraryCall(node, exports.JWT_METHODS.SIGN);
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if this call looks like a JWT verify operation
|
|
209
|
+
*/
|
|
210
|
+
function isVerifyOperation(node) {
|
|
211
|
+
return isJwtLibraryCall(node, exports.JWT_METHODS.VERIFY);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Check if this call looks like a JWT decode operation (no verification)
|
|
215
|
+
*/
|
|
216
|
+
function isDecodeOperation(node) {
|
|
217
|
+
return isJwtLibraryCall(node, exports.JWT_METHODS.DECODE);
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get the options argument from a JWT call
|
|
221
|
+
* For jwt.verify(token, secret, options) -> returns options
|
|
222
|
+
* For jwt.sign(payload, secret, options) -> returns options
|
|
223
|
+
*/
|
|
224
|
+
function getOptionsArgument(node, optionsIndex = 2) {
|
|
225
|
+
const arg = node.arguments[optionsIndex];
|
|
226
|
+
if (arg && arg.type === 'ObjectExpression') {
|
|
227
|
+
return arg;
|
|
228
|
+
}
|
|
229
|
+
return undefined;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Check if file is a test file
|
|
233
|
+
*/
|
|
234
|
+
function isTestFile(filename) {
|
|
235
|
+
return /\.(test|spec)\.[jt]sx?$/.test(filename) ||
|
|
236
|
+
/__(tests?|mocks?)__/.test(filename) ||
|
|
237
|
+
/\/(tests?|specs?|__tests__)\//.test(filename);
|
|
238
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-jwt-security",
|
|
3
|
+
"version": "2.3.0",
|
|
4
|
+
"description": "ESLint plugin for JWT security \u2014 detects algorithm confusion (CVE-2022-23540), alg:none, weak or hardcoded secrets, and decode-without-verify.",
|
|
5
|
+
"type": "commonjs",
|
|
6
|
+
"main": "./dist/src/index.js",
|
|
7
|
+
"types": "./dist/src/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
|
+
"default": "./dist/src/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./types": {
|
|
14
|
+
"types": "./src/types/index.d.ts",
|
|
15
|
+
"default": "./src/types/index.js"
|
|
16
|
+
},
|
|
17
|
+
"./oxlint": {
|
|
18
|
+
"types": "./src/oxlint.d.ts",
|
|
19
|
+
"default": "./src/oxlint.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"author": "Ofri Peretz <ofriperetzdev@gmail.com>",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://eslint.interlace.tools/docs/security/plugin-jwt-security?utm_source=npm&utm_medium=referral&utm_campaign=eslint-plugin-jwt-security-security",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/ofri-peretz/eslint",
|
|
28
|
+
"directory": "packages/eslint-plugin-jwt-security"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/ofri-peretz/eslint/issues"
|
|
32
|
+
},
|
|
33
|
+
"publishConfig": {
|
|
34
|
+
"access": "public"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"src/",
|
|
38
|
+
"dist/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE",
|
|
41
|
+
"CHANGELOG.md",
|
|
42
|
+
"AGENTS.md"
|
|
43
|
+
],
|
|
44
|
+
"keywords": [
|
|
45
|
+
"eslint",
|
|
46
|
+
"eslintplugin",
|
|
47
|
+
"eslint-plugin",
|
|
48
|
+
"static-analysis",
|
|
49
|
+
"linting",
|
|
50
|
+
"code-quality",
|
|
51
|
+
"security",
|
|
52
|
+
"sast",
|
|
53
|
+
"appsec",
|
|
54
|
+
"vulnerability",
|
|
55
|
+
"owasp",
|
|
56
|
+
"cwe",
|
|
57
|
+
"interlace-security",
|
|
58
|
+
"jwt",
|
|
59
|
+
"json-web-token",
|
|
60
|
+
"jwt-security",
|
|
61
|
+
"jsonwebtoken",
|
|
62
|
+
"jose",
|
|
63
|
+
"express-jwt",
|
|
64
|
+
"nestjs-jwt",
|
|
65
|
+
"jwks-rsa",
|
|
66
|
+
"jwt-decode",
|
|
67
|
+
"algorithm-confusion",
|
|
68
|
+
"cve-2022-23540",
|
|
69
|
+
"cwe-347",
|
|
70
|
+
"cwe-757",
|
|
71
|
+
"token",
|
|
72
|
+
"authentication",
|
|
73
|
+
"authorization",
|
|
74
|
+
"llm-optimized",
|
|
75
|
+
"ai-assistant",
|
|
76
|
+
"typescript",
|
|
77
|
+
"auto-fix",
|
|
78
|
+
"auth-security",
|
|
79
|
+
"token-security"
|
|
80
|
+
],
|
|
81
|
+
"engines": {
|
|
82
|
+
"node": ">=18.0.0"
|
|
83
|
+
},
|
|
84
|
+
"dependencies": {
|
|
85
|
+
"tslib": "^2.3.0",
|
|
86
|
+
"@interlace/eslint-devkit": "^1.4.3"
|
|
87
|
+
},
|
|
88
|
+
"peerDependencies": {
|
|
89
|
+
"eslint": "^8.0.0 || ^9.0.0 || ^10.0.0"
|
|
90
|
+
},
|
|
91
|
+
"peerDependenciesMeta": {
|
|
92
|
+
"jsonwebtoken": {
|
|
93
|
+
"optional": true
|
|
94
|
+
},
|
|
95
|
+
"@nestjs/jwt": {
|
|
96
|
+
"optional": true
|
|
97
|
+
},
|
|
98
|
+
"express-jwt": {
|
|
99
|
+
"optional": true
|
|
100
|
+
},
|
|
101
|
+
"jose": {
|
|
102
|
+
"optional": true
|
|
103
|
+
},
|
|
104
|
+
"jwks-rsa": {
|
|
105
|
+
"optional": true
|
|
106
|
+
},
|
|
107
|
+
"jwt-decode": {
|
|
108
|
+
"optional": true
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"devDependencies": {
|
|
112
|
+
"@typescript-eslint/parser": "^8.46.2",
|
|
113
|
+
"@typescript-eslint/rule-tester": "^8.46.2"
|
|
114
|
+
},
|
|
115
|
+
"scripts": {
|
|
116
|
+
"build": "tsx ../../scripts/build-package.ts",
|
|
117
|
+
"test": "vitest run",
|
|
118
|
+
"test:coverage": "vitest run --coverage",
|
|
119
|
+
"typecheck": "tsc -p tsconfig.lib.json --noEmit"
|
|
120
|
+
},
|
|
121
|
+
"funding": {
|
|
122
|
+
"type": "github",
|
|
123
|
+
"url": "https://github.com/ofri-peretz/eslint"
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JWT Ecosystem Interface Compatibility Tests
|
|
3
|
+
*
|
|
4
|
+
* Verifies that JWT-related packages export the interfaces our ESLint rules depend on.
|
|
5
|
+
*
|
|
6
|
+
* @sdk jsonwebtoken, jose, express-jwt, jwks-rsa, jwt-decode
|
|
7
|
+
* @last-updated 2026-01-02
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { describe, it, expect, beforeAll } from 'vitest';
|
|
11
|
+
|
|
12
|
+
// Use unknown types to avoid ESM/CJS type conflicts at compile time
|
|
13
|
+
// Runtime checks handle the actual exports
|
|
14
|
+
let jsonwebtokenModule: unknown;
|
|
15
|
+
let joseModule: unknown;
|
|
16
|
+
let jwtDecodeModule: unknown;
|
|
17
|
+
|
|
18
|
+
// Helper to safely access properties on unknown types
|
|
19
|
+
const getProp = (obj: unknown, key: string): unknown => {
|
|
20
|
+
if (obj && typeof obj === 'object' && key in obj) {
|
|
21
|
+
return (obj as Record<string, unknown>)[key];
|
|
22
|
+
}
|
|
23
|
+
return undefined;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
beforeAll(async () => {
|
|
27
|
+
try {
|
|
28
|
+
jsonwebtokenModule = await import('jsonwebtoken');
|
|
29
|
+
} catch {
|
|
30
|
+
console.warn('jsonwebtoken not installed');
|
|
31
|
+
}
|
|
32
|
+
try {
|
|
33
|
+
joseModule = await import('jose');
|
|
34
|
+
} catch {
|
|
35
|
+
console.warn('jose not installed');
|
|
36
|
+
}
|
|
37
|
+
try {
|
|
38
|
+
jwtDecodeModule = await import('jwt-decode');
|
|
39
|
+
} catch {
|
|
40
|
+
console.warn('jwt-decode not installed');
|
|
41
|
+
}
|
|
42
|
+
}, 30_000); // native-addon packages can take >10s to load cold
|
|
43
|
+
|
|
44
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
45
|
+
// JSONWEBTOKEN INTERFACE
|
|
46
|
+
// Primary package for JWT operations in Node.js
|
|
47
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
48
|
+
|
|
49
|
+
describe('jsonwebtoken Interface Compatibility', () => {
|
|
50
|
+
describe('Core Exports', () => {
|
|
51
|
+
it('exports sign function', () => {
|
|
52
|
+
const sign = getProp(jsonwebtokenModule, 'sign');
|
|
53
|
+
expect(sign).toBeDefined();
|
|
54
|
+
expect(typeof sign).toBe('function');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('exports verify function', () => {
|
|
58
|
+
const verify = getProp(jsonwebtokenModule, 'verify');
|
|
59
|
+
expect(verify).toBeDefined();
|
|
60
|
+
expect(typeof verify).toBe('function');
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('exports decode function', () => {
|
|
64
|
+
const decode = getProp(jsonwebtokenModule, 'decode');
|
|
65
|
+
expect(decode).toBeDefined();
|
|
66
|
+
expect(typeof decode).toBe('function');
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
describe('Sign Options (Critical for Security Rules)', () => {
|
|
71
|
+
it('sign accepts algorithm option', () => {
|
|
72
|
+
// Rules check: no-algorithm-none, require-algorithm-whitelist
|
|
73
|
+
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('sign accepts expiresIn option', () => {
|
|
77
|
+
// Rules check: require-expiration
|
|
78
|
+
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('sign accepts audience option', () => {
|
|
82
|
+
// Rules check: require-audience
|
|
83
|
+
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('sign accepts issuer option', () => {
|
|
87
|
+
// Rules check: require-issuer
|
|
88
|
+
expect(getProp(jsonwebtokenModule, 'sign')).toBeDefined();
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
describe('Verify Options (Critical for Security Rules)', () => {
|
|
93
|
+
it('verify accepts algorithms option (array)', () => {
|
|
94
|
+
// Rules check: verify-algorithm-explicit
|
|
95
|
+
expect(getProp(jsonwebtokenModule, 'verify')).toBeDefined();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('verify accepts complete option', () => {
|
|
99
|
+
// Returns full decoded token with header
|
|
100
|
+
expect(getProp(jsonwebtokenModule, 'verify')).toBeDefined();
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
106
|
+
// JOSE INTERFACE
|
|
107
|
+
// Modern JWT library with async/await support
|
|
108
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
109
|
+
|
|
110
|
+
describe('jose Interface Compatibility', () => {
|
|
111
|
+
describe('Core Exports', () => {
|
|
112
|
+
it('exports SignJWT class', () => {
|
|
113
|
+
expect(getProp(joseModule, 'SignJWT')).toBeDefined();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('exports jwtVerify function', () => {
|
|
117
|
+
const jwtVerify = getProp(joseModule, 'jwtVerify');
|
|
118
|
+
expect(jwtVerify).toBeDefined();
|
|
119
|
+
expect(typeof jwtVerify).toBe('function');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('exports jwtDecrypt function', () => {
|
|
123
|
+
const jwtDecrypt = getProp(joseModule, 'jwtDecrypt');
|
|
124
|
+
expect(jwtDecrypt).toBeDefined();
|
|
125
|
+
expect(typeof jwtDecrypt).toBe('function');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('exports createRemoteJWKSet function', () => {
|
|
129
|
+
// Used for JWKS validation
|
|
130
|
+
const createRemoteJWKSet = getProp(joseModule, 'createRemoteJWKSet');
|
|
131
|
+
expect(createRemoteJWKSet).toBeDefined();
|
|
132
|
+
expect(typeof createRemoteJWKSet).toBe('function');
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('Key Generation', () => {
|
|
137
|
+
it('exports generateKeyPair function', () => {
|
|
138
|
+
expect(getProp(joseModule, 'generateKeyPair')).toBeDefined();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('exports generateSecret function', () => {
|
|
142
|
+
expect(getProp(joseModule, 'generateSecret')).toBeDefined();
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
148
|
+
// JWT-DECODE INTERFACE
|
|
149
|
+
// Client-side JWT decoding (no verification)
|
|
150
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
151
|
+
|
|
152
|
+
describe('jwt-decode Interface Compatibility', () => {
|
|
153
|
+
describe('Core Exports', () => {
|
|
154
|
+
it('exports jwtDecode function', () => {
|
|
155
|
+
// Can be either named export or default export depending on version
|
|
156
|
+
const jwtDecode = getProp(jwtDecodeModule, 'jwtDecode') ?? getProp(jwtDecodeModule, 'default');
|
|
157
|
+
expect(jwtDecode).toBeDefined();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('exports InvalidTokenError', () => {
|
|
161
|
+
expect(getProp(jwtDecodeModule, 'InvalidTokenError')).toBeDefined();
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
167
|
+
// PACKAGE METADATA
|
|
168
|
+
// ═══════════════════════════════════════════════════════════════════════════════
|
|
169
|
+
|
|
170
|
+
describe('Package Metadata', () => {
|
|
171
|
+
it('jsonwebtoken has discoverable version', async () => {
|
|
172
|
+
try {
|
|
173
|
+
const pkgPath = require.resolve('jsonwebtoken/package.json');
|
|
174
|
+
const pkg = await import(pkgPath, { with: { type: 'json' } }).then(m => m.default);
|
|
175
|
+
expect(pkg.version).toBeDefined();
|
|
176
|
+
console.log(`📦 jsonwebtoken version: ${pkg.version}`);
|
|
177
|
+
} catch {
|
|
178
|
+
console.log('📦 jsonwebtoken not installed');
|
|
179
|
+
}
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('jose has discoverable version', async () => {
|
|
183
|
+
try {
|
|
184
|
+
const pkgPath = require.resolve('jose/package.json');
|
|
185
|
+
const pkg = await import(pkgPath, { with: { type: 'json' } }).then(m => m.default);
|
|
186
|
+
expect(pkg.version).toBeDefined();
|
|
187
|
+
console.log(`📦 jose version: ${pkg.version}`);
|
|
188
|
+
} catch {
|
|
189
|
+
console.log('📦 jose not installed');
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import plugin from './index';
|
|
3
|
+
import { rules, configs } from './index';
|
|
4
|
+
|
|
5
|
+
describe('eslint-plugin-jwt-security plugin interface', () => {
|
|
6
|
+
it('should export correct meta information', () => {
|
|
7
|
+
expect(plugin.meta).toBeDefined();
|
|
8
|
+
expect(plugin.meta?.name).toBe('eslint-plugin-jwt-security');
|
|
9
|
+
expect(plugin.meta?.version).toBeDefined();
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('should export all JWT security rules', () => {
|
|
13
|
+
expect(plugin.rules).toBeDefined();
|
|
14
|
+
const ruleNames = Object.keys(plugin.rules || {});
|
|
15
|
+
expect(ruleNames).toContain('no-algorithm-none');
|
|
16
|
+
expect(ruleNames).toContain('no-algorithm-confusion');
|
|
17
|
+
expect(ruleNames).toContain('no-timestamp-manipulation');
|
|
18
|
+
expect(ruleNames.length).toBe(13);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('configurations', () => {
|
|
22
|
+
it('should provide recommended configuration', () => {
|
|
23
|
+
expect(configs.recommended).toBeDefined();
|
|
24
|
+
expect(configs.recommended.plugins?.jwt).toBeDefined();
|
|
25
|
+
|
|
26
|
+
const recommendedRules = configs.recommended.rules || {};
|
|
27
|
+
Object.keys(recommendedRules).forEach(ruleName => {
|
|
28
|
+
expect(ruleName).toMatch(/^jwt\//);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
expect(recommendedRules['jwt/no-algorithm-none']).toBe('error');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should provide strict configuration', () => {
|
|
35
|
+
expect(configs.strict).toBeDefined();
|
|
36
|
+
expect(configs.strict.plugins?.jwt).toBeDefined();
|
|
37
|
+
|
|
38
|
+
const strictRules = configs.strict.rules || {};
|
|
39
|
+
Object.keys(strictRules).forEach(ruleName => {
|
|
40
|
+
expect(ruleName).toMatch(/^jwt\//);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
expect(strictRules['jwt/no-algorithm-none']).toBe('error');
|
|
44
|
+
expect(Object.keys(strictRules).length).toBe(Object.keys(rules).length);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|