332
332
});
333
333
334
334
const jwtInput = $ (' #jwt-input' );
335
- jwtInput .on (' input' , async function () {
336
- decodedJwt = {
337
- header: null ,
338
- payload: null ,
339
- signature: null
340
- };
341
-
342
- const jwt = $ (this ).text ();
343
- if (jwt) {
344
- try {
345
- const parts = jwt .split (' .' );
346
- if (parts .length === 3 ) {
347
- const header = JSON .parse (atob (parts[0 ]));
348
- const payload = JSON .parse (atob (parts[1 ]));
349
- const signature = parts[2 ];
350
-
351
- decodedJwt = {
352
- header: header,
353
- payload: payload,
354
- signature: signature
355
- };
356
- await showDecodedJwt (parts, header, payload, signature);
357
- colorJwtInput ($ (this ), parts);
358
- } else {
359
- showError (' Invalid JWT format. A JWT should have three parts separated by dots.' );
360
- }
361
- } catch (e) {
362
- showError (' Error decoding JWT: ' + e .message );
363
- }
364
- } else {
365
- await showDecodedJwt (null , null , null , ' ' );
366
- }
335
+ let debounceTimeout;
336
+ jwtInput .on (' input' , function () {
337
+ clearTimeout (debounceTimeout);
338
+ debounceTimeout = setTimeout (async () => await parseJwt .call (this ), 300 ); // 300ms debounce
367
339
});
368
340
369
341
@if (Model .View ? .Token != null )
372
344
}
373
345
else
374
346
{
375
- @: await showDecodedJwt (null , null , null , ' ' );
347
+ @: await showDecodedJwt (null , null , ' ' , ' ' , ' ' );
376
348
}
377
349
}
378
350
379
- function colorJwtInput (target , parts ) {
351
+ async function parseJwt () {
352
+ decodedJwt = {
353
+ header: null ,
354
+ payload: null ,
355
+ signature: null
356
+ };
357
+
358
+ // Clear previous state before attempting to parse a new JWT
359
+ await showDecodedJwt (null , null , ' ' , ' ' , ' ' );
360
+
361
+ const jwt = $ (this ).text ();
362
+ if (jwt) {
363
+ try {
364
+ const parts = jwt .indexOf (' .' ) === - 1 ? [jwt] : jwt .split (' .' );
365
+ const headerInfo = parseJwtPart (parts, 0 );
366
+ const payloadInfo = parseJwtPart (parts, 1 );
367
+ const signature = parts .length > 2 ? parts[2 ] : ' ' ;
368
+
369
+ decodedJwt = {
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 ] : ' ' ,
374
+ signature: signature
375
+ };
376
+
377
+ await showDecodedJwt (decodedJwt .header , decodedJwt .payload , decodedJwt .encodedHeader , decodedJwt .encodedPayload , signature);
378
+ colorJwtInput ($ (this ), parts, decodedJwt .encodedHeader , decodedJwt .encodedPayload , decodedJwt .signature );
379
+
380
+ if (parts .length !== 3 ) {
381
+ showError (' Invalid JWT format. A JWT should have three parts separated by dots.' );
382
+ }
383
+ } catch (e) {
384
+ showError (' Error decoding JWT: ' + e .message );
385
+ }
386
+ } else {
387
+ await showDecodedJwt (null , null , ' ' , ' ' , ' ' );
388
+ }
389
+ }
390
+
391
+ function parseJwtPart (parts , index ) {
392
+ if (index < 0 || index >= parts .length ) {
393
+ return [];
394
+ }
395
+
396
+ const part = parts[index];
397
+ if (! part) {
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
417
+ }
418
+
419
+ try {
420
+ const encodedPart = part .substring (jsonStartPos, jsonEndPos);
421
+ const decodedPart = decodeBase64UrlSafe (encodedPart);
422
+ return [JSON .parse (decodedPart), encodedPart];
423
+ }
424
+ catch {
425
+
426
+ }
427
+
428
+ return [];
429
+ }
430
+
431
+ function colorJwtInput (target , originalParts , encodedHeader , encodedPayload , signature ) {
380
432
let html = ' ' ;
381
- if (parts .length > 0 ) {
382
- 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
+ }
383
450
}
384
- if (parts .length > 1 ) {
385
- 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
+ }
386
468
}
387
- if (parts . length > 2 ) {
388
- 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>` ;
389
471
}
390
472
target .html (html);
391
473
}
392
474
393
- async function showDecodedJwt (jwtParts , header , payload , signature ) {
475
+ async function showDecodedJwt (header , payload , encodedHeader , encodedPayload , signature ) {
394
476
clearError ();
395
477
hideSignatureValidationResults ();
396
478
399
481
400
482
$ (' #jwt-signature' ).text (signature || ' ' );
401
483
402
- if (jwtParts && Array . isArray (jwtParts) && jwtParts . length === 3 ) {
403
- await attemptSignatureValidation (header, payload, jwtParts );
484
+ if (encodedHeader && encodedPayload && signature ) {
485
+ await attemptSignatureValidation (header, payload, encodedHeader, encodedPayload, signature );
404
486
}
405
487
}
406
488
517
599
return contents;
518
600
}
519
601
520
- async function attemptSignatureValidation (header , payload , jwtParts ) {
602
+ async function attemptSignatureValidation (header , payload , encodedHeader , encodedPayload , signature ) {
521
603
const jwksUrlField = $ (' #jwks-url' );
522
604
523
605
const isPristine = jwksUrlField .data (' pristine' ) !== false ;
530
612
531
613
if (jwksUrl) {
532
614
await loadJwks (jwksUrl);
533
-
615
+
616
+ const headerAndPayload = encodedHeader + ' .' + encodedPayload;
534
617
if (jwks .keys .length === 0 ) {
535
- showSignatureValidationResult (' warning' , ' No JWKs loaded. Cannot validate signature.' );
618
+ if (signature) {
619
+ showSignatureValidationResult (' warning' , ' The JWT has a signature, but no JWKs could be loaded to verify whether the signature is valid.' );
620
+ }
621
+ else {
622
+ showSignatureValidationResult (' warning' , ' No JWKs loaded and no signature to validate.' );
623
+ }
536
624
return ;
537
625
}
538
-
539
- const headerAndPayload = jwtParts[0 ] + ' .' + jwtParts[1 ];
540
- const signature = jwtParts[2 ];
626
+
541
627
const result = await validateSignature (header, headerAndPayload, signature, jwks .keys );
542
628
543
629
if (result .signatureValidated ) {
620
706
[' verify' ]
621
707
);
622
708
623
- signature = signature .replace (/ -/ g , ' +' ).replace (/ _/ g , ' /' ); // Replace URL-safe characters with standard Base64 characters
624
- const binarySignature = atob (signature);
709
+ const binarySignature = decodeBase64UrlSafe (signature);
625
710
const signatureBuffer = Uint8Array .from (binarySignature, c => c .charCodeAt (0 ));
626
711
const isValid = await subtle .verify (algorithm, publicKey, signatureBuffer, new TextEncoder ().encode (headerAndPayload));
627
712
return { signatureValidated: true , isValid: isValid };
658
743
return null ;
659
744
}
660
745
661
- if (! response .headers .get (' Content-Type' ).startsWith (' application/json' )) {
746
+ const contentType = response .headers .get (' Content-Type' );
747
+ if (! contentType || ! contentType .startsWith (' application/json' )) {
662
748
if (url .toLowerCase ().indexOf (' .well-known' ) !== - 1 || url .toLowerCase ().endsWith (' .json' )) {
663
749
showError (' The provided JWKs URL does not return a valid JSON response.' );
664
750
return null ;
707
793
function hideSignatureValidationResults () {
708
794
$ (' .jwt-signature-validation-result' ).removeClass (' d-flex' ).addClass (' d-none' );
709
795
}
796
+
797
+ function decodeBase64UrlSafe (base64Data ) {
798
+ return atob (base64Data .replace (/ -/ g , ' +' ).replace (/ _/ g , ' /' ));
799
+ }
710
800
< / script>
711
801
}
0 commit comments