|
344 | 344 | }
|
345 | 345 | else
|
346 | 346 | {
|
347 |
| - @:await showDecodedJwt(null, null, null, ' '); |
| 347 | + @:await showDecodedJwt(null, null, '', '', ' '); |
348 | 348 | }
|
349 | 349 | }
|
350 | 350 |
|
|
356 | 356 | };
|
357 | 357 |
|
358 | 358 | // Clear previous state before attempting to parse a new JWT
|
359 |
| - await showDecodedJwt(null, null, null, ' '); |
| 359 | + await showDecodedJwt(null, null, '', '', ' '); |
360 | 360 |
|
361 | 361 | const jwt = $(this).text();
|
362 | 362 | if (jwt) {
|
363 | 363 | try {
|
364 | 364 | const parts = jwt.indexOf('.') === -1 ? [jwt] : jwt.split('.');
|
365 |
| - const header = parseJwtPart(parts, 0); |
366 |
| - const payload = parseJwtPart(parts, 1); |
| 365 | + const headerInfo = parseJwtPart(parts, 0); |
| 366 | + const payloadInfo = parseJwtPart(parts, 1); |
367 | 367 | const signature = parts.length > 2 ? parts[2] : '';
|
368 | 368 |
|
369 | 369 | decodedJwt = {
|
370 |
| - header: header, |
371 |
| - payload: payload, |
| 370 | + header: headerInfo.length > 0 ? headerInfo[0] : null, |
| 371 | + encodedHeader: headerInfo.length > 1 ? headerInfo[1] : '', |
| 372 | + payload: payloadInfo.length > 0 ? payloadInfo[0] : null, |
| 373 | + encodedPayload: payloadInfo.length > 1 ? payloadInfo[1] : '', |
372 | 374 | signature: signature
|
373 | 375 | };
|
374 | 376 |
|
375 |
| - await showDecodedJwt(parts, header, payload, signature); |
376 |
| - colorJwtInput($(this), parts); |
| 377 | + await showDecodedJwt(decodedJwt.header, decodedJwt.payload, decodedJwt.encodedHeader, decodedJwt.encodedPayload, signature); |
| 378 | + colorJwtInput($(this), parts, decodedJwt.encodedHeader, decodedJwt.encodedPayload, decodedJwt.signature); |
377 | 379 |
|
378 | 380 | if (parts.length !== 3) {
|
379 | 381 | showError('Invalid JWT format. A JWT should have three parts separated by dots.');
|
|
382 | 384 | showError('Error decoding JWT: ' + e.message);
|
383 | 385 | }
|
384 | 386 | } else {
|
385 |
| - await showDecodedJwt(null, null, null, ' '); |
| 387 | + await showDecodedJwt(null, null, '', '', ' '); |
386 | 388 | }
|
387 | 389 | }
|
388 | 390 |
|
389 | 391 | function parseJwtPart(parts, index) {
|
390 | 392 | if (index < 0 || index >= parts.length) {
|
391 |
| - return null; |
| 393 | + return []; |
392 | 394 | }
|
393 | 395 |
|
394 | 396 | const part = parts[index];
|
395 | 397 | if (!part) {
|
396 |
| - return null; |
| 398 | + return []; |
| 399 | + } |
| 400 | + |
| 401 | + // Find the base64 marker for a JSON object to start parsing |
| 402 | + let jsonStartPos = 0; |
| 403 | + while (jsonStartPos < part.length && part.substring(jsonStartPos, jsonStartPos + 3) !== 'eyJ') { |
| 404 | + jsonStartPos++; |
| 405 | + } |
| 406 | + if (jsonStartPos >= part.length) { |
| 407 | + return []; // No JSON object found |
| 408 | + } |
| 409 | + |
| 410 | + // Find the base64 marker for a JSON object to end parsing ('In0' for '"}' or 'fQ' for '}') |
| 411 | + let jsonEndPos = part.length; |
| 412 | + while (jsonEndPos > jsonStartPos && (part.substring(jsonEndPos - 3, jsonEndPos) !== 'In0') && (part.substring(jsonEndPos - 2, jsonEndPos) !== 'fQ')) { |
| 413 | + jsonEndPos--; |
| 414 | + } |
| 415 | + if (jsonEndPos <= jsonStartPos) { |
| 416 | + return []; // No valid JSON object found |
397 | 417 | }
|
398 | 418 |
|
399 | 419 | try {
|
400 |
| - const decodedPart = decodeBase64UrlSafe(part); |
401 |
| - return JSON.parse(decodedPart); |
| 420 | + const encodedPart = part.substring(jsonStartPos, jsonEndPos); |
| 421 | + const decodedPart = decodeBase64UrlSafe(encodedPart); |
| 422 | + return [JSON.parse(decodedPart), encodedPart]; |
402 | 423 | }
|
403 | 424 | catch {
|
404 | 425 |
|
405 | 426 | }
|
406 | 427 |
|
407 |
| - return null; |
| 428 | + return []; |
408 | 429 | }
|
409 | 430 |
|
410 |
| - function colorJwtInput(target, parts) { |
| 431 | + function colorJwtInput(target, originalParts, encodedHeader, encodedPayload, signature) { |
411 | 432 | let html = '';
|
412 |
| - if (parts.length > 0) { |
413 |
| - html += `<span class="text-danger">${parts[0] || ''}</span>`; |
| 433 | + if (encodedHeader) { |
| 434 | + const originalHeader = originalParts[0]; |
| 435 | + if (originalHeader === encodedHeader) { |
| 436 | + html += `<span class="text-danger">${encodedHeader}</span>`; |
| 437 | + } |
| 438 | + else { |
| 439 | + // Show the additional characters before and after the "encodedHeader" part that are found in originalHeader |
| 440 | + const start = originalHeader.indexOf(encodedHeader); |
| 441 | + const end = start + encodedHeader.length; |
| 442 | + if (start > 0) { |
| 443 | + html += `<span class="skipped">${originalHeader.substring(0, start)}</span>`; |
| 444 | + } |
| 445 | + html += `<span class="text-danger">${encodedHeader}</span>`; |
| 446 | + if (end < originalHeader.length) { |
| 447 | + html += `<span class="skipped">${originalHeader.substring(end)}</span>`; |
| 448 | + } |
| 449 | + } |
414 | 450 | }
|
415 |
| - if (parts.length > 1) { |
416 |
| - html += `<span class="text-success">.${parts[1] || ''}</span>`; |
| 451 | + if (encodedPayload) { |
| 452 | + const originalPayload = originalParts[1]; |
| 453 | + if (originalPayload === encodedPayload) { |
| 454 | + html += '<span class="jwt-divider">.</span>' + `<span class="text-success">${encodedPayload}</span>`; |
| 455 | + } |
| 456 | + else { |
| 457 | + // Show the additional characters before and after the "encodedPayload" part that are found in originalPayload |
| 458 | + const start = originalPayload.indexOf(encodedPayload); |
| 459 | + const end = start + encodedPayload.length; |
| 460 | + if (start > 0) { |
| 461 | + html += `<span class="skipped">${originalPayload.substring(0, start)}</span>`; |
| 462 | + } |
| 463 | + html += '<span class="jwt-divider">.</span>' + `<span class="text-success">${encodedPayload}</span>`; |
| 464 | + if (end < originalPayload.length) { |
| 465 | + html += `<span class="skipped">${originalPayload.substring(end)}</span>`; |
| 466 | + } |
| 467 | + } |
417 | 468 | }
|
418 |
| - if (parts.length > 2) { |
419 |
| - html += `<span class="text-warning">.${parts.slice(2).join('.') || ''}</span>`; |
| 469 | + if (signature) { |
| 470 | + html += '<span class="jwt-divider">.</span>' + `<span class="text-warning">${signature}</span>`; |
420 | 471 | }
|
421 | 472 | target.html(html);
|
422 | 473 | }
|
423 | 474 |
|
424 |
| - async function showDecodedJwt(jwtParts, header, payload, signature) { |
| 475 | + async function showDecodedJwt(header, payload, encodedHeader, encodedPayload, signature) { |
425 | 476 | clearError();
|
426 | 477 | hideSignatureValidationResults();
|
427 | 478 |
|
|
430 | 481 |
|
431 | 482 | $('#jwt-signature').text(signature || ' ');
|
432 | 483 |
|
433 |
| - if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) { |
434 |
| - await attemptSignatureValidation(header, payload, jwtParts); |
| 484 | + if (encodedHeader && encodedPayload && signature) { |
| 485 | + await attemptSignatureValidation(header, payload, encodedHeader, encodedPayload, signature); |
435 | 486 | }
|
436 | 487 | }
|
437 | 488 |
|
|
548 | 599 | return contents;
|
549 | 600 | }
|
550 | 601 |
|
551 |
| - async function attemptSignatureValidation(header, payload, jwtParts) { |
| 602 | + async function attemptSignatureValidation(header, payload, encodedHeader, encodedPayload, signature) { |
552 | 603 | const jwksUrlField = $('#jwks-url');
|
553 | 604 |
|
554 | 605 | const isPristine = jwksUrlField.data('pristine') !== false;
|
|
562 | 613 | if (jwksUrl) {
|
563 | 614 | await loadJwks(jwksUrl);
|
564 | 615 |
|
565 |
| - const headerAndPayload = jwtParts[0] + '.' + jwtParts[1]; |
566 |
| - const signature = jwtParts[2]; |
567 |
| - |
| 616 | + const headerAndPayload = encodedHeader + '.' + encodedPayload; |
568 | 617 | if (jwks.keys.length === 0) {
|
569 | 618 | if (signature) {
|
570 | 619 | showSignatureValidationResult('warning', 'The JWT has a signature, but no JWKs could be loaded to verify whether the signature is valid.');
|
|
0 commit comments