@claude-agent/jwt-explain 1.0.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/bin/cli.js +331 -0
- package/package.json +37 -0
- package/src/index.js +653 -0
package/bin/cli.js
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* jwt-explain CLI
|
|
5
|
+
* Decode and explain JWT tokens in plain English
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
'use strict';
|
|
9
|
+
|
|
10
|
+
const {
|
|
11
|
+
decode,
|
|
12
|
+
validate,
|
|
13
|
+
explain,
|
|
14
|
+
format,
|
|
15
|
+
isExpired,
|
|
16
|
+
getTimeUntilExpiry,
|
|
17
|
+
getRegisteredClaims,
|
|
18
|
+
getCommonClaims,
|
|
19
|
+
getAlgorithms
|
|
20
|
+
} = require('../src/index.js');
|
|
21
|
+
|
|
22
|
+
const HELP = `
|
|
23
|
+
jwt-explain - Decode and explain JWT tokens in plain English
|
|
24
|
+
|
|
25
|
+
USAGE:
|
|
26
|
+
jwt-explain <token> Explain a JWT token
|
|
27
|
+
jwt-explain -d <token> Decode and show raw parts
|
|
28
|
+
jwt-explain -v <token> Validate token structure
|
|
29
|
+
jwt-explain -e <token> Check if token is expired
|
|
30
|
+
jwt-explain --claims List registered JWT claims
|
|
31
|
+
jwt-explain --algorithms List signing algorithms
|
|
32
|
+
|
|
33
|
+
OPTIONS:
|
|
34
|
+
-d, --decode Decode and show raw header/payload JSON
|
|
35
|
+
-v, --validate Validate token structure
|
|
36
|
+
-e, --expiry Show expiration status
|
|
37
|
+
-D, --detailed Show detailed explanations
|
|
38
|
+
--no-color Disable colored output
|
|
39
|
+
-j, --json Output as JSON
|
|
40
|
+
--claims List all registered claims
|
|
41
|
+
--algorithms List all algorithms
|
|
42
|
+
-h, --help Show this help
|
|
43
|
+
-V, --version Show version
|
|
44
|
+
|
|
45
|
+
EXAMPLES:
|
|
46
|
+
jwt-explain eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
47
|
+
jwt-explain -d "Bearer eyJhbGciOiJIUzI1NiI..."
|
|
48
|
+
jwt-explain -v eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
49
|
+
jwt-explain -e eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
|
50
|
+
jwt-explain --claims
|
|
51
|
+
jwt-explain --algorithms
|
|
52
|
+
|
|
53
|
+
NOTES:
|
|
54
|
+
- This tool decodes JWTs but does NOT verify signatures
|
|
55
|
+
- Never trust a decoded JWT without verification
|
|
56
|
+
- "Bearer " prefix is automatically stripped
|
|
57
|
+
|
|
58
|
+
Built autonomously by Claude (Anthropic's AI)
|
|
59
|
+
https://github.com/claude-agent-tools/jwt-explain
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
function main() {
|
|
63
|
+
const args = process.argv.slice(2);
|
|
64
|
+
|
|
65
|
+
// Parse options
|
|
66
|
+
let mode = 'explain';
|
|
67
|
+
let detailed = false;
|
|
68
|
+
let useColor = process.stdout.isTTY !== false;
|
|
69
|
+
let json = false;
|
|
70
|
+
let token = null;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < args.length; i++) {
|
|
73
|
+
const arg = args[i];
|
|
74
|
+
|
|
75
|
+
if (arg === '-h' || arg === '--help') {
|
|
76
|
+
console.log(HELP);
|
|
77
|
+
process.exit(0);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (arg === '-V' || arg === '--version') {
|
|
81
|
+
const pkg = require('../package.json');
|
|
82
|
+
console.log(pkg.version);
|
|
83
|
+
process.exit(0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (arg === '-D' || arg === '--detailed') {
|
|
87
|
+
detailed = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (arg === '--no-color') {
|
|
92
|
+
useColor = false;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (arg === '-j' || arg === '--json') {
|
|
97
|
+
json = true;
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (arg === '-d' || arg === '--decode') {
|
|
102
|
+
mode = 'decode';
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (arg === '-v' || arg === '--validate') {
|
|
107
|
+
mode = 'validate';
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (arg === '-e' || arg === '--expiry') {
|
|
112
|
+
mode = 'expiry';
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (arg === '--claims') {
|
|
117
|
+
mode = 'claims';
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (arg === '--algorithms') {
|
|
122
|
+
mode = 'algorithms';
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (arg.startsWith('-')) {
|
|
127
|
+
console.error(`Unknown option: ${arg}`);
|
|
128
|
+
console.error('Use --help for usage information');
|
|
129
|
+
process.exit(2);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
token = arg;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle info modes
|
|
136
|
+
if (mode === 'claims') {
|
|
137
|
+
const claims = { ...getRegisteredClaims(), ...getCommonClaims() };
|
|
138
|
+
if (json) {
|
|
139
|
+
console.log(JSON.stringify(claims, null, 2));
|
|
140
|
+
} else {
|
|
141
|
+
const bold = useColor ? '\x1b[1m' : '';
|
|
142
|
+
const cyan = useColor ? '\x1b[36m' : '';
|
|
143
|
+
const blue = useColor ? '\x1b[34m' : '';
|
|
144
|
+
const magenta = useColor ? '\x1b[35m' : '';
|
|
145
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
146
|
+
|
|
147
|
+
console.log(`${bold}Registered Claims:${reset}`);
|
|
148
|
+
console.log('');
|
|
149
|
+
for (const [key, info] of Object.entries(getRegisteredClaims())) {
|
|
150
|
+
console.log(` ${blue}${key.padEnd(10)}${reset} ${info.name} - ${info.description}`);
|
|
151
|
+
}
|
|
152
|
+
console.log('');
|
|
153
|
+
console.log(`${bold}Common Claims:${reset}`);
|
|
154
|
+
console.log('');
|
|
155
|
+
for (const [key, info] of Object.entries(getCommonClaims())) {
|
|
156
|
+
console.log(` ${magenta}${key.padEnd(15)}${reset} ${info.name} - ${info.description}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
process.exit(0);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (mode === 'algorithms') {
|
|
163
|
+
const algs = getAlgorithms();
|
|
164
|
+
if (json) {
|
|
165
|
+
console.log(JSON.stringify(algs, null, 2));
|
|
166
|
+
} else {
|
|
167
|
+
const bold = useColor ? '\x1b[1m' : '';
|
|
168
|
+
const cyan = useColor ? '\x1b[36m' : '';
|
|
169
|
+
const green = useColor ? '\x1b[32m' : '';
|
|
170
|
+
const yellow = useColor ? '\x1b[33m' : '';
|
|
171
|
+
const red = useColor ? '\x1b[31m' : '';
|
|
172
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
173
|
+
|
|
174
|
+
console.log(`${bold}JWT Signing Algorithms:${reset}`);
|
|
175
|
+
console.log('');
|
|
176
|
+
|
|
177
|
+
const byType = { symmetric: [], asymmetric: [], none: [] };
|
|
178
|
+
for (const [key, info] of Object.entries(algs)) {
|
|
179
|
+
byType[info.type].push({ key, ...info });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
console.log(`${bold}Symmetric (shared secret):${reset}`);
|
|
183
|
+
for (const alg of byType.symmetric) {
|
|
184
|
+
console.log(` ${green}${alg.key.padEnd(8)}${reset} ${alg.description}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
console.log('');
|
|
188
|
+
console.log(`${bold}Asymmetric (public/private key):${reset}`);
|
|
189
|
+
for (const alg of byType.asymmetric) {
|
|
190
|
+
console.log(` ${cyan}${alg.key.padEnd(8)}${reset} ${alg.description}`);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.log('');
|
|
194
|
+
console.log(`${bold}Insecure:${reset}`);
|
|
195
|
+
for (const alg of byType.none) {
|
|
196
|
+
console.log(` ${red}${alg.key.padEnd(8)}${reset} ${alg.description}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Modes that require a token
|
|
203
|
+
if (!token) {
|
|
204
|
+
console.error('Error: No JWT token provided');
|
|
205
|
+
console.error('Usage: jwt-explain <token>');
|
|
206
|
+
console.error('Use --help for more information');
|
|
207
|
+
process.exit(2);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Handle different modes
|
|
211
|
+
switch (mode) {
|
|
212
|
+
case 'decode': {
|
|
213
|
+
const decoded = decode(token);
|
|
214
|
+
if (!decoded) {
|
|
215
|
+
console.error('Error: Invalid JWT token format');
|
|
216
|
+
process.exit(1);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (json) {
|
|
220
|
+
console.log(JSON.stringify({
|
|
221
|
+
header: decoded.header,
|
|
222
|
+
payload: decoded.payload,
|
|
223
|
+
signature: decoded.signature
|
|
224
|
+
}, null, 2));
|
|
225
|
+
} else {
|
|
226
|
+
const bold = useColor ? '\x1b[1m' : '';
|
|
227
|
+
const cyan = useColor ? '\x1b[36m' : '';
|
|
228
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
229
|
+
|
|
230
|
+
console.log(`${bold}Header:${reset}`);
|
|
231
|
+
console.log(cyan + JSON.stringify(decoded.header, null, 2) + reset);
|
|
232
|
+
console.log('');
|
|
233
|
+
console.log(`${bold}Payload:${reset}`);
|
|
234
|
+
console.log(cyan + JSON.stringify(decoded.payload, null, 2) + reset);
|
|
235
|
+
console.log('');
|
|
236
|
+
console.log(`${bold}Signature:${reset}`);
|
|
237
|
+
console.log(decoded.signature);
|
|
238
|
+
}
|
|
239
|
+
break;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
case 'validate': {
|
|
243
|
+
const result = validate(token);
|
|
244
|
+
|
|
245
|
+
if (json) {
|
|
246
|
+
console.log(JSON.stringify(result, null, 2));
|
|
247
|
+
} else {
|
|
248
|
+
const bold = useColor ? '\x1b[1m' : '';
|
|
249
|
+
const green = useColor ? '\x1b[32m' : '';
|
|
250
|
+
const red = useColor ? '\x1b[31m' : '';
|
|
251
|
+
const yellow = useColor ? '\x1b[33m' : '';
|
|
252
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
253
|
+
|
|
254
|
+
console.log(`${bold}Validation Result:${reset}`);
|
|
255
|
+
console.log('');
|
|
256
|
+
|
|
257
|
+
if (result.valid) {
|
|
258
|
+
console.log(`${green}✓ Token has valid JWT structure${reset}`);
|
|
259
|
+
} else {
|
|
260
|
+
console.log(`${red}✗ Token has invalid JWT structure${reset}`);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (result.errors.length > 0) {
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(`${bold}Errors:${reset}`);
|
|
266
|
+
for (const error of result.errors) {
|
|
267
|
+
console.log(` ${red}✗ ${error}${reset}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (result.warnings.length > 0) {
|
|
272
|
+
console.log('');
|
|
273
|
+
console.log(`${bold}Warnings:${reset}`);
|
|
274
|
+
for (const warning of result.warnings) {
|
|
275
|
+
console.log(` ${yellow}⚠ ${warning}${reset}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
process.exit(result.valid ? 0 : 1);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
case 'expiry': {
|
|
284
|
+
const result = getTimeUntilExpiry(token);
|
|
285
|
+
|
|
286
|
+
if (result.error) {
|
|
287
|
+
console.error(`Error: ${result.error}`);
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (json) {
|
|
292
|
+
console.log(JSON.stringify(result, null, 2));
|
|
293
|
+
} else {
|
|
294
|
+
const bold = useColor ? '\x1b[1m' : '';
|
|
295
|
+
const green = useColor ? '\x1b[32m' : '';
|
|
296
|
+
const red = useColor ? '\x1b[31m' : '';
|
|
297
|
+
const yellow = useColor ? '\x1b[33m' : '';
|
|
298
|
+
const reset = useColor ? '\x1b[0m' : '';
|
|
299
|
+
|
|
300
|
+
if (result.neverExpires) {
|
|
301
|
+
console.log(`${yellow}⚠ Token has no expiration${reset}`);
|
|
302
|
+
} else if (result.expired) {
|
|
303
|
+
console.log(`${red}✗ Token expired ${result.expiredAgoText} ago${reset}`);
|
|
304
|
+
} else {
|
|
305
|
+
console.log(`${green}✓ Token expires in ${result.expiresInText}${reset}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
process.exit(result.expired ? 1 : 0);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
case 'explain':
|
|
313
|
+
default: {
|
|
314
|
+
const result = explain(token);
|
|
315
|
+
|
|
316
|
+
if (json) {
|
|
317
|
+
// Remove circular references and functions for JSON output
|
|
318
|
+
const jsonSafe = JSON.parse(JSON.stringify(result));
|
|
319
|
+
console.log(JSON.stringify(jsonSafe, null, 2));
|
|
320
|
+
} else {
|
|
321
|
+
console.log(format(result, { color: useColor, detailed }));
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (!result.valid) process.exit(1);
|
|
325
|
+
if (result.validation && !result.validation.valid) process.exit(1);
|
|
326
|
+
break;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
main();
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@claude-agent/jwt-explain",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Decode and explain JWT tokens in plain English - understand headers, claims, and expiration",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"jwt-explain": "bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/*.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"jwt",
|
|
14
|
+
"json-web-token",
|
|
15
|
+
"decode",
|
|
16
|
+
"explain",
|
|
17
|
+
"parse",
|
|
18
|
+
"token",
|
|
19
|
+
"auth",
|
|
20
|
+
"authentication",
|
|
21
|
+
"cli"
|
|
22
|
+
],
|
|
23
|
+
"author": "Claude Agent <claude-agent@agentmail.to>",
|
|
24
|
+
"license": "MIT",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/claude-agent-tools/jwt-explain.git"
|
|
28
|
+
},
|
|
29
|
+
"homepage": "https://github.com/claude-agent-tools/jwt-explain#readme",
|
|
30
|
+
"engines": {
|
|
31
|
+
"node": ">=14.0.0"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src/",
|
|
35
|
+
"bin/"
|
|
36
|
+
]
|
|
37
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* jwt-explain
|
|
3
|
+
* Decode and explain JWT tokens in plain English
|
|
4
|
+
* Zero dependencies
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
// Standard JWT claims and their descriptions
|
|
10
|
+
const REGISTERED_CLAIMS = {
|
|
11
|
+
iss: { name: 'Issuer', description: 'Who issued this token' },
|
|
12
|
+
sub: { name: 'Subject', description: 'Who/what this token is about' },
|
|
13
|
+
aud: { name: 'Audience', description: 'Who this token is intended for' },
|
|
14
|
+
exp: { name: 'Expiration Time', description: 'When this token expires', isDate: true },
|
|
15
|
+
nbf: { name: 'Not Before', description: 'Token not valid before this time', isDate: true },
|
|
16
|
+
iat: { name: 'Issued At', description: 'When this token was issued', isDate: true },
|
|
17
|
+
jti: { name: 'JWT ID', description: 'Unique identifier for this token' }
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// Common custom claims
|
|
21
|
+
const COMMON_CLAIMS = {
|
|
22
|
+
name: { name: 'Name', description: 'Full name of the user' },
|
|
23
|
+
given_name: { name: 'Given Name', description: 'First name' },
|
|
24
|
+
family_name: { name: 'Family Name', description: 'Last name' },
|
|
25
|
+
email: { name: 'Email', description: 'Email address' },
|
|
26
|
+
email_verified: { name: 'Email Verified', description: 'Whether email has been verified' },
|
|
27
|
+
phone_number: { name: 'Phone Number', description: 'Phone number' },
|
|
28
|
+
picture: { name: 'Picture', description: 'Profile picture URL' },
|
|
29
|
+
locale: { name: 'Locale', description: 'User locale/language' },
|
|
30
|
+
zoneinfo: { name: 'Time Zone', description: 'User time zone' },
|
|
31
|
+
updated_at: { name: 'Updated At', description: 'Last profile update time', isDate: true },
|
|
32
|
+
auth_time: { name: 'Auth Time', description: 'When authentication occurred', isDate: true },
|
|
33
|
+
nonce: { name: 'Nonce', description: 'Value to prevent replay attacks' },
|
|
34
|
+
acr: { name: 'Auth Context Class', description: 'Authentication context class reference' },
|
|
35
|
+
amr: { name: 'Auth Methods', description: 'Authentication methods used' },
|
|
36
|
+
azp: { name: 'Authorized Party', description: 'The party the token was issued to' },
|
|
37
|
+
at_hash: { name: 'Access Token Hash', description: 'Hash of the access token' },
|
|
38
|
+
c_hash: { name: 'Code Hash', description: 'Hash of the authorization code' },
|
|
39
|
+
scope: { name: 'Scope', description: 'Permissions granted by this token' },
|
|
40
|
+
permissions: { name: 'Permissions', description: 'Specific permissions granted' },
|
|
41
|
+
roles: { name: 'Roles', description: 'User roles' },
|
|
42
|
+
groups: { name: 'Groups', description: 'Group memberships' }
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Algorithm descriptions
|
|
46
|
+
const ALGORITHMS = {
|
|
47
|
+
HS256: { name: 'HMAC SHA-256', type: 'symmetric', description: 'HMAC with SHA-256 (symmetric key)' },
|
|
48
|
+
HS384: { name: 'HMAC SHA-384', type: 'symmetric', description: 'HMAC with SHA-384 (symmetric key)' },
|
|
49
|
+
HS512: { name: 'HMAC SHA-512', type: 'symmetric', description: 'HMAC with SHA-512 (symmetric key)' },
|
|
50
|
+
RS256: { name: 'RSA SHA-256', type: 'asymmetric', description: 'RSA signature with SHA-256' },
|
|
51
|
+
RS384: { name: 'RSA SHA-384', type: 'asymmetric', description: 'RSA signature with SHA-384' },
|
|
52
|
+
RS512: { name: 'RSA SHA-512', type: 'asymmetric', description: 'RSA signature with SHA-512' },
|
|
53
|
+
ES256: { name: 'ECDSA P-256', type: 'asymmetric', description: 'ECDSA using P-256 curve and SHA-256' },
|
|
54
|
+
ES384: { name: 'ECDSA P-384', type: 'asymmetric', description: 'ECDSA using P-384 curve and SHA-384' },
|
|
55
|
+
ES512: { name: 'ECDSA P-521', type: 'asymmetric', description: 'ECDSA using P-521 curve and SHA-512' },
|
|
56
|
+
PS256: { name: 'RSA-PSS SHA-256', type: 'asymmetric', description: 'RSA-PSS signature with SHA-256' },
|
|
57
|
+
PS384: { name: 'RSA-PSS SHA-384', type: 'asymmetric', description: 'RSA-PSS signature with SHA-384' },
|
|
58
|
+
PS512: { name: 'RSA-PSS SHA-512', type: 'asymmetric', description: 'RSA-PSS signature with SHA-512' },
|
|
59
|
+
EdDSA: { name: 'Edwards-curve DSA', type: 'asymmetric', description: 'Edwards-curve Digital Signature Algorithm' },
|
|
60
|
+
none: { name: 'None', type: 'none', description: 'Unsigned token (insecure!)' }
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Base64URL decode
|
|
65
|
+
* @param {string} str - Base64URL encoded string
|
|
66
|
+
* @returns {string} Decoded string
|
|
67
|
+
*/
|
|
68
|
+
function base64UrlDecode(str) {
|
|
69
|
+
// Replace URL-safe characters
|
|
70
|
+
let base64 = str.replace(/-/g, '+').replace(/_/g, '/');
|
|
71
|
+
|
|
72
|
+
// Add padding if needed
|
|
73
|
+
while (base64.length % 4) {
|
|
74
|
+
base64 += '=';
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Decode
|
|
78
|
+
return Buffer.from(base64, 'base64').toString('utf8');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Base64URL encode
|
|
83
|
+
* @param {string} str - String to encode
|
|
84
|
+
* @returns {string} Base64URL encoded string
|
|
85
|
+
*/
|
|
86
|
+
function base64UrlEncode(str) {
|
|
87
|
+
return Buffer.from(str, 'utf8')
|
|
88
|
+
.toString('base64')
|
|
89
|
+
.replace(/\+/g, '-')
|
|
90
|
+
.replace(/\//g, '_')
|
|
91
|
+
.replace(/=+$/, '');
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Decode a JWT token without verification
|
|
96
|
+
* @param {string} token - JWT token string
|
|
97
|
+
* @returns {object|null} Decoded token or null if invalid
|
|
98
|
+
*/
|
|
99
|
+
function decode(token) {
|
|
100
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
token = token.trim();
|
|
105
|
+
|
|
106
|
+
// Remove "Bearer " prefix if present
|
|
107
|
+
if (token.toLowerCase().startsWith('bearer ')) {
|
|
108
|
+
token = token.slice(7).trim();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const parts = token.split('.');
|
|
112
|
+
|
|
113
|
+
if (parts.length !== 3) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const headerJson = base64UrlDecode(parts[0]);
|
|
119
|
+
const payloadJson = base64UrlDecode(parts[1]);
|
|
120
|
+
|
|
121
|
+
const header = JSON.parse(headerJson);
|
|
122
|
+
const payload = JSON.parse(payloadJson);
|
|
123
|
+
|
|
124
|
+
return {
|
|
125
|
+
token,
|
|
126
|
+
header,
|
|
127
|
+
payload,
|
|
128
|
+
signature: parts[2],
|
|
129
|
+
parts: {
|
|
130
|
+
header: parts[0],
|
|
131
|
+
payload: parts[1],
|
|
132
|
+
signature: parts[2]
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
} catch (e) {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Check if a token is a valid JWT structure
|
|
142
|
+
* @param {string} token - Token to validate
|
|
143
|
+
* @returns {object} Validation result
|
|
144
|
+
*/
|
|
145
|
+
function validate(token) {
|
|
146
|
+
const errors = [];
|
|
147
|
+
const warnings = [];
|
|
148
|
+
|
|
149
|
+
if (typeof token !== 'string' || !token.trim()) {
|
|
150
|
+
return { valid: false, errors: ['Token must be a non-empty string'], warnings: [] };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
token = token.trim();
|
|
154
|
+
|
|
155
|
+
// Remove Bearer prefix
|
|
156
|
+
if (token.toLowerCase().startsWith('bearer ')) {
|
|
157
|
+
token = token.slice(7).trim();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const parts = token.split('.');
|
|
161
|
+
|
|
162
|
+
if (parts.length !== 3) {
|
|
163
|
+
errors.push(`JWT must have 3 parts separated by dots, found ${parts.length}`);
|
|
164
|
+
return { valid: false, errors, warnings };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Check each part is valid base64url
|
|
168
|
+
const partNames = ['header', 'payload', 'signature'];
|
|
169
|
+
for (let i = 0; i < 3; i++) {
|
|
170
|
+
if (!parts[i]) {
|
|
171
|
+
// Empty signature is valid for alg: none tokens
|
|
172
|
+
if (i === 2) continue; // Check signature later after decoding header
|
|
173
|
+
errors.push(`${partNames[i]} part is empty`);
|
|
174
|
+
} else if (!/^[A-Za-z0-9_-]+$/.test(parts[i])) {
|
|
175
|
+
errors.push(`${partNames[i]} contains invalid base64url characters`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (errors.length > 0) {
|
|
180
|
+
return { valid: false, errors, warnings };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Try to decode header and payload
|
|
184
|
+
let header, payload;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const headerJson = base64UrlDecode(parts[0]);
|
|
188
|
+
header = JSON.parse(headerJson);
|
|
189
|
+
} catch {
|
|
190
|
+
errors.push('Header is not valid JSON');
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
try {
|
|
194
|
+
const payloadJson = base64UrlDecode(parts[1]);
|
|
195
|
+
payload = JSON.parse(payloadJson);
|
|
196
|
+
} catch {
|
|
197
|
+
errors.push('Payload is not valid JSON');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (errors.length > 0) {
|
|
201
|
+
return { valid: false, errors, warnings };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Validate header structure
|
|
205
|
+
if (typeof header !== 'object' || header === null) {
|
|
206
|
+
errors.push('Header must be a JSON object');
|
|
207
|
+
} else {
|
|
208
|
+
if (!header.alg) {
|
|
209
|
+
errors.push('Header missing required "alg" (algorithm) field');
|
|
210
|
+
} else if (header.alg === 'none') {
|
|
211
|
+
warnings.push('Algorithm is "none" - token is unsigned and should not be trusted');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!header.typ) {
|
|
215
|
+
warnings.push('Header missing "typ" field (optional but recommended)');
|
|
216
|
+
} else if (header.typ !== 'JWT') {
|
|
217
|
+
warnings.push(`Header "typ" is "${header.typ}", expected "JWT"`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Validate payload structure
|
|
222
|
+
if (typeof payload !== 'object' || payload === null) {
|
|
223
|
+
errors.push('Payload must be a JSON object');
|
|
224
|
+
} else {
|
|
225
|
+
// Check expiration
|
|
226
|
+
if (payload.exp) {
|
|
227
|
+
const expTime = payload.exp * 1000;
|
|
228
|
+
if (expTime < Date.now()) {
|
|
229
|
+
warnings.push('Token has expired');
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
warnings.push('Token has no expiration (exp) - may be valid indefinitely');
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Check not before
|
|
236
|
+
if (payload.nbf) {
|
|
237
|
+
const nbfTime = payload.nbf * 1000;
|
|
238
|
+
if (nbfTime > Date.now()) {
|
|
239
|
+
warnings.push('Token is not yet valid (nbf is in the future)');
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Check issued at
|
|
244
|
+
if (payload.iat) {
|
|
245
|
+
const iatTime = payload.iat * 1000;
|
|
246
|
+
if (iatTime > Date.now()) {
|
|
247
|
+
warnings.push('Token issued in the future (iat is in the future)');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
valid: errors.length === 0,
|
|
254
|
+
errors,
|
|
255
|
+
warnings
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if a token is expired
|
|
261
|
+
* @param {string} token - JWT token
|
|
262
|
+
* @returns {object} Expiration status
|
|
263
|
+
*/
|
|
264
|
+
function isExpired(token) {
|
|
265
|
+
const decoded = decode(token);
|
|
266
|
+
|
|
267
|
+
if (!decoded) {
|
|
268
|
+
return { error: 'Invalid token', expired: null, expiresAt: null };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const { payload } = decoded;
|
|
272
|
+
|
|
273
|
+
if (!payload.exp) {
|
|
274
|
+
return { expired: false, expiresAt: null, neverExpires: true };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const expiresAt = new Date(payload.exp * 1000);
|
|
278
|
+
const now = new Date();
|
|
279
|
+
const expired = expiresAt < now;
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
expired,
|
|
283
|
+
expiresAt,
|
|
284
|
+
expiresIn: expired ? null : expiresAt - now,
|
|
285
|
+
expiredAgo: expired ? now - expiresAt : null
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Get the time until expiration
|
|
291
|
+
* @param {string} token - JWT token
|
|
292
|
+
* @returns {object} Time until expiration
|
|
293
|
+
*/
|
|
294
|
+
function getTimeUntilExpiry(token) {
|
|
295
|
+
const result = isExpired(token);
|
|
296
|
+
|
|
297
|
+
if (result.error) {
|
|
298
|
+
return { error: result.error };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (result.neverExpires) {
|
|
302
|
+
return { neverExpires: true };
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (result.expired) {
|
|
306
|
+
const ago = result.expiredAgo;
|
|
307
|
+
return {
|
|
308
|
+
expired: true,
|
|
309
|
+
expiredAgo: ago,
|
|
310
|
+
expiredAgoText: formatDuration(ago)
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
const remaining = result.expiresIn;
|
|
315
|
+
return {
|
|
316
|
+
expired: false,
|
|
317
|
+
expiresIn: remaining,
|
|
318
|
+
expiresInText: formatDuration(remaining)
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format a duration in milliseconds to human-readable
|
|
324
|
+
* @param {number} ms - Duration in milliseconds
|
|
325
|
+
* @returns {string}
|
|
326
|
+
*/
|
|
327
|
+
function formatDuration(ms) {
|
|
328
|
+
const seconds = Math.floor(ms / 1000);
|
|
329
|
+
const minutes = Math.floor(seconds / 60);
|
|
330
|
+
const hours = Math.floor(minutes / 60);
|
|
331
|
+
const days = Math.floor(hours / 24);
|
|
332
|
+
|
|
333
|
+
if (days > 0) {
|
|
334
|
+
const remainingHours = hours % 24;
|
|
335
|
+
return `${days} day${days !== 1 ? 's' : ''}${remainingHours > 0 ? `, ${remainingHours} hour${remainingHours !== 1 ? 's' : ''}` : ''}`;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
if (hours > 0) {
|
|
339
|
+
const remainingMinutes = minutes % 60;
|
|
340
|
+
return `${hours} hour${hours !== 1 ? 's' : ''}${remainingMinutes > 0 ? `, ${remainingMinutes} minute${remainingMinutes !== 1 ? 's' : ''}` : ''}`;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (minutes > 0) {
|
|
344
|
+
const remainingSeconds = seconds % 60;
|
|
345
|
+
return `${minutes} minute${minutes !== 1 ? 's' : ''}${remainingSeconds > 0 ? `, ${remainingSeconds} second${remainingSeconds !== 1 ? 's' : ''}` : ''}`;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
return `${seconds} second${seconds !== 1 ? 's' : ''}`;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Explain a claim
|
|
353
|
+
* @param {string} key - Claim key
|
|
354
|
+
* @param {any} value - Claim value
|
|
355
|
+
* @returns {object} Claim explanation
|
|
356
|
+
*/
|
|
357
|
+
function explainClaim(key, value) {
|
|
358
|
+
const registeredInfo = REGISTERED_CLAIMS[key];
|
|
359
|
+
const commonInfo = COMMON_CLAIMS[key];
|
|
360
|
+
const info = registeredInfo || commonInfo;
|
|
361
|
+
|
|
362
|
+
const result = {
|
|
363
|
+
key,
|
|
364
|
+
value,
|
|
365
|
+
isRegistered: !!registeredInfo,
|
|
366
|
+
isCommon: !!commonInfo
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
if (info) {
|
|
370
|
+
result.name = info.name;
|
|
371
|
+
result.description = info.description;
|
|
372
|
+
|
|
373
|
+
if (info.isDate && typeof value === 'number') {
|
|
374
|
+
result.dateValue = new Date(value * 1000);
|
|
375
|
+
result.dateFormatted = result.dateValue.toISOString();
|
|
376
|
+
}
|
|
377
|
+
} else {
|
|
378
|
+
result.name = key;
|
|
379
|
+
result.description = 'Custom claim';
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Format value display
|
|
383
|
+
if (typeof value === 'object' && value !== null) {
|
|
384
|
+
result.displayValue = JSON.stringify(value);
|
|
385
|
+
} else {
|
|
386
|
+
result.displayValue = String(value);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
return result;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Explain a JWT token in plain English
|
|
394
|
+
* @param {string} token - JWT token
|
|
395
|
+
* @returns {object} Token explanation
|
|
396
|
+
*/
|
|
397
|
+
function explain(token) {
|
|
398
|
+
const decoded = decode(token);
|
|
399
|
+
|
|
400
|
+
if (!decoded) {
|
|
401
|
+
return {
|
|
402
|
+
valid: false,
|
|
403
|
+
error: 'Invalid JWT token format',
|
|
404
|
+
input: token
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const validation = validate(token);
|
|
409
|
+
const expiry = getTimeUntilExpiry(token);
|
|
410
|
+
const { header, payload } = decoded;
|
|
411
|
+
|
|
412
|
+
// Explain algorithm
|
|
413
|
+
const algInfo = ALGORITHMS[header.alg] || { name: header.alg, description: 'Unknown algorithm' };
|
|
414
|
+
|
|
415
|
+
// Explain header
|
|
416
|
+
const headerExplanation = {
|
|
417
|
+
algorithm: {
|
|
418
|
+
value: header.alg,
|
|
419
|
+
name: algInfo.name,
|
|
420
|
+
type: algInfo.type,
|
|
421
|
+
description: algInfo.description
|
|
422
|
+
},
|
|
423
|
+
type: header.typ || null,
|
|
424
|
+
keyId: header.kid || null,
|
|
425
|
+
other: {}
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
// Capture any other header fields
|
|
429
|
+
for (const [key, value] of Object.entries(header)) {
|
|
430
|
+
if (!['alg', 'typ', 'kid'].includes(key)) {
|
|
431
|
+
headerExplanation.other[key] = value;
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Explain claims
|
|
436
|
+
const claims = [];
|
|
437
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
438
|
+
claims.push(explainClaim(key, value));
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// Sort claims: registered first, then common, then custom
|
|
442
|
+
claims.sort((a, b) => {
|
|
443
|
+
if (a.isRegistered && !b.isRegistered) return -1;
|
|
444
|
+
if (!a.isRegistered && b.isRegistered) return 1;
|
|
445
|
+
if (a.isCommon && !b.isCommon) return -1;
|
|
446
|
+
if (!a.isCommon && b.isCommon) return 1;
|
|
447
|
+
return 0;
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// Generate summary
|
|
451
|
+
let summary = `${algInfo.name} signed JWT`;
|
|
452
|
+
|
|
453
|
+
if (payload.sub) {
|
|
454
|
+
summary += ` for subject "${payload.sub}"`;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
if (payload.iss) {
|
|
458
|
+
summary += ` from issuer "${payload.iss}"`;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (expiry.expired) {
|
|
462
|
+
summary += ` (expired ${expiry.expiredAgoText} ago)`;
|
|
463
|
+
} else if (expiry.neverExpires) {
|
|
464
|
+
summary += ' (no expiration)';
|
|
465
|
+
} else {
|
|
466
|
+
summary += ` (expires in ${expiry.expiresInText})`;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
return {
|
|
470
|
+
valid: true,
|
|
471
|
+
input: token,
|
|
472
|
+
validation,
|
|
473
|
+
header: headerExplanation,
|
|
474
|
+
claims,
|
|
475
|
+
expiry,
|
|
476
|
+
summary,
|
|
477
|
+
decoded
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Format output for display
|
|
483
|
+
* @param {object} result - Explain result
|
|
484
|
+
* @param {object} options - Formatting options
|
|
485
|
+
* @returns {string}
|
|
486
|
+
*/
|
|
487
|
+
function format(result, options = {}) {
|
|
488
|
+
const { color = true, detailed = false } = options;
|
|
489
|
+
|
|
490
|
+
const c = {
|
|
491
|
+
reset: color ? '\x1b[0m' : '',
|
|
492
|
+
bold: color ? '\x1b[1m' : '',
|
|
493
|
+
dim: color ? '\x1b[2m' : '',
|
|
494
|
+
cyan: color ? '\x1b[36m' : '',
|
|
495
|
+
green: color ? '\x1b[32m' : '',
|
|
496
|
+
yellow: color ? '\x1b[33m' : '',
|
|
497
|
+
red: color ? '\x1b[31m' : '',
|
|
498
|
+
magenta: color ? '\x1b[35m' : '',
|
|
499
|
+
blue: color ? '\x1b[34m' : ''
|
|
500
|
+
};
|
|
501
|
+
|
|
502
|
+
const lines = [];
|
|
503
|
+
|
|
504
|
+
if (!result.valid) {
|
|
505
|
+
lines.push(`${c.red}Error:${c.reset} ${result.error}`);
|
|
506
|
+
return lines.join('\n');
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// Summary
|
|
510
|
+
lines.push(`${c.bold}JWT Token${c.reset}`);
|
|
511
|
+
lines.push('');
|
|
512
|
+
lines.push(`${c.bold}Summary:${c.reset} ${result.summary}`);
|
|
513
|
+
|
|
514
|
+
// Validation status
|
|
515
|
+
if (result.validation.errors.length > 0 || result.validation.warnings.length > 0) {
|
|
516
|
+
lines.push('');
|
|
517
|
+
lines.push(`${c.bold}Validation:${c.reset}`);
|
|
518
|
+
for (const error of result.validation.errors) {
|
|
519
|
+
lines.push(` ${c.red}✗ ${error}${c.reset}`);
|
|
520
|
+
}
|
|
521
|
+
for (const warning of result.validation.warnings) {
|
|
522
|
+
lines.push(` ${c.yellow}⚠ ${warning}${c.reset}`);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Expiry status
|
|
527
|
+
lines.push('');
|
|
528
|
+
lines.push(`${c.bold}Expiration:${c.reset}`);
|
|
529
|
+
if (result.expiry.neverExpires) {
|
|
530
|
+
lines.push(` ${c.yellow}⚠ No expiration set${c.reset}`);
|
|
531
|
+
} else if (result.expiry.expired) {
|
|
532
|
+
lines.push(` ${c.red}✗ Expired ${result.expiry.expiredAgoText} ago${c.reset}`);
|
|
533
|
+
} else {
|
|
534
|
+
lines.push(` ${c.green}✓ Valid for ${result.expiry.expiresInText}${c.reset}`);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Header
|
|
538
|
+
lines.push('');
|
|
539
|
+
lines.push(`${c.bold}Header:${c.reset}`);
|
|
540
|
+
lines.push(` ${c.cyan}Algorithm:${c.reset} ${result.header.algorithm.value} (${result.header.algorithm.description})`);
|
|
541
|
+
if (result.header.type) {
|
|
542
|
+
lines.push(` ${c.cyan}Type:${c.reset} ${result.header.type}`);
|
|
543
|
+
}
|
|
544
|
+
if (result.header.keyId) {
|
|
545
|
+
lines.push(` ${c.cyan}Key ID:${c.reset} ${result.header.keyId}`);
|
|
546
|
+
}
|
|
547
|
+
for (const [key, value] of Object.entries(result.header.other)) {
|
|
548
|
+
lines.push(` ${c.cyan}${key}:${c.reset} ${JSON.stringify(value)}`);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Claims
|
|
552
|
+
lines.push('');
|
|
553
|
+
lines.push(`${c.bold}Claims:${c.reset}`);
|
|
554
|
+
|
|
555
|
+
for (const claim of result.claims) {
|
|
556
|
+
const badge = claim.isRegistered ? `${c.blue}[registered]${c.reset}` :
|
|
557
|
+
claim.isCommon ? `${c.magenta}[common]${c.reset}` :
|
|
558
|
+
`${c.dim}[custom]${c.reset}`;
|
|
559
|
+
|
|
560
|
+
if (claim.dateValue) {
|
|
561
|
+
lines.push(` ${c.yellow}${claim.key}${c.reset} ${badge}`);
|
|
562
|
+
lines.push(` ${claim.dateFormatted}`);
|
|
563
|
+
if (detailed) {
|
|
564
|
+
lines.push(` ${c.dim}${claim.name}: ${claim.description}${c.reset}`);
|
|
565
|
+
}
|
|
566
|
+
} else {
|
|
567
|
+
lines.push(` ${c.yellow}${claim.key}${c.reset} = ${claim.displayValue} ${badge}`);
|
|
568
|
+
if (detailed) {
|
|
569
|
+
lines.push(` ${c.dim}${claim.name}: ${claim.description}${c.reset}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// Detailed mode: show raw parts
|
|
575
|
+
if (detailed) {
|
|
576
|
+
lines.push('');
|
|
577
|
+
lines.push(`${c.bold}Raw Parts:${c.reset}`);
|
|
578
|
+
lines.push(` ${c.dim}Header:${c.reset} ${result.decoded.parts.header.substring(0, 50)}...`);
|
|
579
|
+
lines.push(` ${c.dim}Payload:${c.reset} ${result.decoded.parts.payload.substring(0, 50)}...`);
|
|
580
|
+
lines.push(` ${c.dim}Signature:${c.reset} ${result.decoded.parts.signature.substring(0, 50)}...`);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return lines.join('\n');
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Create a simple JWT for testing (unsigned)
|
|
588
|
+
* @param {object} payload - Payload object
|
|
589
|
+
* @param {object} header - Header object (optional)
|
|
590
|
+
* @returns {string}
|
|
591
|
+
*/
|
|
592
|
+
function createUnsigned(payload, header = {}) {
|
|
593
|
+
const fullHeader = {
|
|
594
|
+
alg: 'none',
|
|
595
|
+
typ: 'JWT',
|
|
596
|
+
...header
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
const headerB64 = base64UrlEncode(JSON.stringify(fullHeader));
|
|
600
|
+
const payloadB64 = base64UrlEncode(JSON.stringify(payload));
|
|
601
|
+
|
|
602
|
+
return `${headerB64}.${payloadB64}.`;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Get all registered claims info
|
|
607
|
+
* @returns {object}
|
|
608
|
+
*/
|
|
609
|
+
function getRegisteredClaims() {
|
|
610
|
+
return { ...REGISTERED_CLAIMS };
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Get all common claims info
|
|
615
|
+
* @returns {object}
|
|
616
|
+
*/
|
|
617
|
+
function getCommonClaims() {
|
|
618
|
+
return { ...COMMON_CLAIMS };
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Get all algorithm info
|
|
623
|
+
* @returns {object}
|
|
624
|
+
*/
|
|
625
|
+
function getAlgorithms() {
|
|
626
|
+
return { ...ALGORITHMS };
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
module.exports = {
|
|
630
|
+
// Core functions
|
|
631
|
+
decode,
|
|
632
|
+
validate,
|
|
633
|
+
explain,
|
|
634
|
+
format,
|
|
635
|
+
|
|
636
|
+
// Utilities
|
|
637
|
+
isExpired,
|
|
638
|
+
getTimeUntilExpiry,
|
|
639
|
+
createUnsigned,
|
|
640
|
+
base64UrlDecode,
|
|
641
|
+
base64UrlEncode,
|
|
642
|
+
|
|
643
|
+
// Info getters
|
|
644
|
+
getRegisteredClaims,
|
|
645
|
+
getCommonClaims,
|
|
646
|
+
getAlgorithms,
|
|
647
|
+
explainClaim,
|
|
648
|
+
|
|
649
|
+
// Constants
|
|
650
|
+
REGISTERED_CLAIMS,
|
|
651
|
+
COMMON_CLAIMS,
|
|
652
|
+
ALGORITHMS
|
|
653
|
+
};
|