-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathapp.js
More file actions
396 lines (335 loc) · 16.8 KB
/
app.js
File metadata and controls
396 lines (335 loc) · 16.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
document.addEventListener('DOMContentLoaded', () => {
// --- DOM Elements ---
const tabs = document.querySelectorAll('.tab-button');
const tabContents = document.querySelectorAll('.tab-content');
// Signer elements
const headerInput = document.getElementById('header');
const payloadInput = document.getElementById('payload');
const secretSignInput = document.getElementById('secret-sign');
const algorithmSelect = document.getElementById('algorithm');
const generateButton = document.getElementById('generate-button');
const generatedJwtOutput = document.getElementById('generated-jwt');
const copyJwtButton = document.getElementById('copy-jwt-button'); // Added button reference
// const headerHighlight = document.getElementById('header-highlight'); // Removed
// const payloadHighlight = document.getElementById('payload-highlight'); // Removed
const headerError = document.getElementById('header-error');
const payloadError = document.getElementById('payload-error');
const toggleSecretSignButton = document.getElementById('toggle-secret-sign');
// Verifier elements
const jwtVerifyInput = document.getElementById('jwt-verify');
const secretVerifyInput = document.getElementById('secret-verify');
const verifyButton = document.getElementById('verify-button');
const verificationStatusOutput = document.getElementById('verification-status');
const decodedHeaderOutput = document.getElementById('decoded-header');
const decodedPayloadOutput = document.getElementById('decoded-payload');
const toggleSecretVerifyButton = document.getElementById('toggle-secret-verify');
const themeToggleButton = document.getElementById('theme-toggle'); // Added theme toggle button
// --- Helper Functions ---
// Base64URL Encode (RFC 4648 Section 5)
function base64UrlEncode(str) {
// Ensure UTF-8 encoding before Base64 encoding
let base64 = btoa(unescape(encodeURIComponent(str)));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Base64URL Decode
function base64UrlDecode(str) {
str = str.replace(/-/g, '+').replace(/_/g, '/');
// Pad with '=' characters if necessary
while (str.length % 4) {
str += '=';
}
try {
// Decode Base64 and then URI-decode the UTF-8 characters
return decodeURIComponent(escape(atob(str)));
} catch (e) {
console.error("Base64URL Decode Error:", e);
return null; // Indicate failure
}
}
// Get HMAC-SHA function based on algorithm
function getHashingFunction(alg) {
switch (alg) {
case 'HS256': return CryptoJS.HmacSHA256;
case 'HS384': return CryptoJS.HmacSHA384;
case 'HS512': return CryptoJS.HmacSHA512;
default: throw new Error('Unsupported algorithm');
}
}
// Format JSON for display
function formatJson(jsonString) {
try {
const obj = JSON.parse(jsonString);
return JSON.stringify(obj, null, 2); // Pretty print
} catch (e) {
// If it's not valid JSON, return the original string as is
// This is important for displaying potentially malformed decoded parts
return jsonString;
}
}
// Validate JSON in textarea, update styles and error message
function validateJsonInput(textarea, errorElement) {
const code = textarea.value;
let isValidJson = true;
let errorMessage = '';
try {
// Attempt to parse. If it fails, it's invalid.
JSON.parse(code);
textarea.classList.remove('invalid-json'); // Is valid
} catch (e) {
isValidJson = false;
errorMessage = `Invalid JSON: ${e.message.split('\n')[0]}`; // Keep error concise
textarea.classList.add('invalid-json'); // Is invalid
}
// Update the dedicated error message element below the textarea
if (errorElement) {
errorElement.textContent = errorMessage;
}
// No highlighting needed for the input textarea itself with Prism
return isValidJson;
}
// Highlight generic code blocks (like decoded outputs)
function highlightCodeBlock(codeElement) {
Prism.highlightElement(codeElement);
}
// --- Tab Switching ---
window.showTab = (tabName) => {
tabContents.forEach(content => {
content.classList.remove('active');
});
tabs.forEach(tab => {
tab.classList.remove('active');
});
document.getElementById(tabName).classList.add('active');
document.querySelector(`.tab-button[onclick="showTab('${tabName}')"]`).classList.add('active');
// Re-validate relevant inputs when switching tabs
if (tabName === 'signer') {
validateJsonInput(headerInput, headerError);
validateJsonInput(payloadInput, payloadError);
} else if (tabName === 'verifier') {
// Re-highlight decoded outputs (which use <pre><code>) if they have content
if (decodedHeaderOutput.textContent) highlightCodeBlock(decodedHeaderOutput);
if (decodedPayloadOutput.textContent) highlightCodeBlock(decodedPayloadOutput);
}
};
// --- Event Listeners ---
// Initial validation for default signer view
validateJsonInput(headerInput, headerError);
validateJsonInput(payloadInput, payloadError);
// Real-time JSON validation for Signer inputs
headerInput.addEventListener('input', () => validateJsonInput(headerInput, headerError));
payloadInput.addEventListener('input', () => validateJsonInput(payloadInput, payloadError));
// Generate JWT Button
generateButton.addEventListener('click', () => {
// Re-validate JSON on button click, in case of copy-paste
const isHeaderValid = validateJsonInput(headerInput, headerError);
const isPayloadValid = validateJsonInput(payloadInput, payloadError);
generatedJwtOutput.value = ''; // Clear previous output/error
if (!isHeaderValid || !isPayloadValid) {
generatedJwtOutput.value = 'Error: Invalid JSON in Header or Payload.';
return;
}
const headerStr = headerInput.value;
const payloadStr = payloadInput.value;
const secret = secretSignInput.value;
const alg = algorithmSelect.value;
if (!secret) {
generatedJwtOutput.value = 'Error: Secret Key is required.';
return;
}
try {
const headerObj = JSON.parse(headerStr);
// Ensure the selected algorithm matches the header 'alg' property if present
// Or add/update the 'alg' property in the header based on selection
if (!headerObj.alg || headerObj.alg !== alg) {
console.log(`Updating header 'alg' property to selected value: ${alg}`);
headerObj.alg = alg;
// Update the textarea and validation status to reflect the change
const updatedHeaderStr = JSON.stringify(headerObj, null, 2);
headerInput.value = updatedHeaderStr;
validateJsonInput(headerInput, headerError); // Re-validate the updated header
}
// Use the potentially modified header object for encoding
const encodedHeader = base64UrlEncode(JSON.stringify(headerObj));
const encodedPayload = base64UrlEncode(payloadStr); // Payload is kept as original string input
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
const hashingFunction = getHashingFunction(alg);
// Use CryptoJS.enc.Utf8.parse for the message if it contains UTF-8 chars?
// Let's stick to raw string for now as per common examples.
const signature = hashingFunction(unsignedToken, secret);
// Convert the CryptoJS WordArray signature to a Base64URL string
// CryptoJS Hmac returns WordArray. We need raw bytes -> Base64 -> Base64URL
const base64Signature = CryptoJS.enc.Base64.stringify(signature);
const encodedSignature = base64Signature.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
generatedJwtOutput.value = `${unsignedToken}.${encodedSignature}`;
} catch (error) {
console.error("JWT Generation Error:", error);
generatedJwtOutput.value = `Error generating JWT: ${error.message}`;
}
});
// Verify JWT Button
verifyButton.addEventListener('click', () => {
const token = jwtVerifyInput.value.trim();
const secret = secretVerifyInput.value;
// Clear previous results
verificationStatusOutput.textContent = '';
verificationStatusOutput.className = ''; // Reset classes
decodedHeaderOutput.textContent = '';
decodedPayloadOutput.textContent = '';
if (!token) {
verificationStatusOutput.textContent = 'Please enter a JWT.';
verificationStatusOutput.classList.add('invalid');
return;
}
if (!secret && token.split('.').length === 3) { // Only require secret if it looks like a signed JWT
// Allow decoding unsigned JWTs? For now, require secret for verification.
verificationStatusOutput.textContent = 'Secret Key is required for verification.';
verificationStatusOutput.classList.add('invalid');
return;
}
const parts = token.split('.');
if (parts.length !== 3) {
// Could potentially handle unsecured JWTs (2 parts) here if desired
verificationStatusOutput.textContent = 'Invalid JWT format (must have 3 parts separated by dots).';
verificationStatusOutput.classList.add('invalid');
return;
}
const [encodedHeader, encodedPayload, encodedSignature] = parts;
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
// Decode Header and Payload
let headerStr, payloadStr;
let headerObj = null;
let decodeError = false;
headerStr = base64UrlDecode(encodedHeader);
payloadStr = base64UrlDecode(encodedPayload);
if (headerStr === null) {
decodedHeaderOutput.textContent = 'Error decoding header (Invalid Base64URL)';
decodeError = true;
} else {
decodedHeaderOutput.textContent = formatJson(headerStr); // Format if possible
highlightCodeBlock(decodedHeaderOutput);
try {
headerObj = JSON.parse(headerStr); // Needed for algorithm check
} catch (e) {
decodedHeaderOutput.textContent += `\n\n(Warning: Invalid JSON: ${e.message.split('\n')[0]})`;
// Don't set headerObj, verification will fail later due to missing alg
}
}
if (payloadStr === null) {
decodedPayloadOutput.textContent = 'Error decoding payload (Invalid Base64URL)';
decodeError = true;
} else {
decodedPayloadOutput.textContent = formatJson(payloadStr); // Format if possible
highlightCodeBlock(decodedPayloadOutput);
try {
JSON.parse(payloadStr); // Validate payload JSON but don't need the object
} catch (e) {
decodedPayloadOutput.textContent += `\n\n(Warning: Invalid JSON: ${e.message.split('\n')[0]})`;
}
}
if (decodeError) {
verificationStatusOutput.textContent = 'Error decoding JWT parts.';
verificationStatusOutput.classList.add('invalid');
return; // Stop if decoding failed
}
// Verify Signature
try {
if (!headerObj || !headerObj.alg) {
verificationStatusOutput.textContent = 'Verification failed: Algorithm (alg) missing or invalid in header.';
verificationStatusOutput.classList.add('invalid');
return;
}
const alg = headerObj.alg;
if (!['HS256', 'HS384', 'HS512'].includes(alg)) {
verificationStatusOutput.textContent = `Verification failed: Unsupported algorithm: ${alg}`;
verificationStatusOutput.classList.add('invalid');
return;
}
const hashingFunction = getHashingFunction(alg);
// Calculate the expected signature
const expectedSignatureWordArray = hashingFunction(unsignedToken, secret);
// Convert the expected signature (WordArray) to Base64URL
const expectedBase64Signature = CryptoJS.enc.Base64.stringify(expectedSignatureWordArray);
const expectedEncodedSignature = expectedBase64Signature.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
// Compare the received signature with the expected one
if (encodedSignature === expectedEncodedSignature) {
verificationStatusOutput.textContent = 'Signature Valid';
verificationStatusOutput.classList.add('valid');
} else {
verificationStatusOutput.textContent = 'Signature Invalid';
verificationStatusOutput.classList.add('invalid');
}
} catch (error) {
console.error("JWT Verification Error:", error);
verificationStatusOutput.textContent = `Error during signature verification: ${error.message}`;
verificationStatusOutput.classList.add('invalid');
}
});
// Toggle Secret Visibility
function toggleSecretVisibility(inputElement, buttonElement) {
const isPassword = inputElement.type === 'password';
inputElement.type = isPassword ? 'text' : 'password';
buttonElement.textContent = isPassword ? 'Hide' : 'Show';
}
toggleSecretSignButton.addEventListener('click', () => {
toggleSecretVisibility(secretSignInput, toggleSecretSignButton);
});
toggleSecretVerifyButton.addEventListener('click', () => {
toggleSecretVisibility(secretVerifyInput, toggleSecretVerifyButton);
});
// Algorithm select change updates header 'alg' property automatically
algorithmSelect.addEventListener('change', () => {
const currentHeaderValue = headerInput.value;
try {
// Attempt to parse header JSON. If valid, update alg.
const headerObj = JSON.parse(currentHeaderValue);
headerObj.alg = algorithmSelect.value;
headerInput.value = JSON.stringify(headerObj, null, 2);
// Re-validate the updated header
validateJsonInput(headerInput, headerError);
} catch (e) {
// If header is not valid JSON, don't try to update it.
// The generate function will handle adding 'alg' if missing later.
console.warn("Could not auto-update header algorithm: Header is not valid JSON.");
}
});
// Copy JWT Button Event Listener
copyJwtButton.addEventListener('click', () => {
const jwtToCopy = generatedJwtOutput.value;
if (jwtToCopy && navigator.clipboard) {
navigator.clipboard.writeText(jwtToCopy).then(() => {
// Visual feedback for success
const originalText = copyJwtButton.textContent;
const originalBgColor = copyJwtButton.style.backgroundColor;
copyJwtButton.textContent = 'Copied!';
copyJwtButton.style.backgroundColor = '#2ecc71'; // Green feedback
copyJwtButton.disabled = true; // Briefly disable button
setTimeout(() => {
copyJwtButton.textContent = originalText;
copyJwtButton.style.backgroundColor = originalBgColor; // Restore original color
copyJwtButton.disabled = false; // Re-enable button
}, 1500); // Reset after 1.5 seconds
}).catch(err => {
console.error('Failed to copy JWT to clipboard: ', err);
// Optional: Provide visual error feedback to the user here
alert('Failed to copy JWT. See console for details.');
});
} else if (!navigator.clipboard) {
console.warn('Clipboard API not available in this browser.');
// Optional: Fallback mechanism or alert for older browsers
alert('Clipboard API not available in this browser.');
}
});
// --- Theme Toggling ---
const currentTheme = localStorage.getItem('theme');
if (currentTheme === 'dark') {
document.body.classList.add('dark-mode');
}
themeToggleButton.addEventListener('click', () => {
document.body.classList.toggle('dark-mode');
let theme = 'light';
if (document.body.classList.contains('dark-mode')) {
theme = 'dark';
}
localStorage.setItem('theme', theme);
});
}); // End DOMContentLoaded