Skip to content

Commit 022f1b3

Browse files
committed
Rewrote JSON formatting to use HTML rather than stringify
1 parent 8f7d317 commit 022f1b3

File tree

1 file changed

+146
-82
lines changed

1 file changed

+146
-82
lines changed

src/Pages/Home/JwtDecoder/JwtDecoder.cshtml

Lines changed: 146 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
@page "~/jwt-decoder"
1+
@page "~/jwt-decoder"
22
@model IdentityServerHost.Pages.Home.JwtDecoder
33

44
<!-- Google Tag Manager (noscript) -->
@@ -113,126 +113,100 @@
113113
presenterMode: false,
114114
explainClaims: false
115115
};
116-
117-
function defaultJsonStringifyReplacer(key, value) {
118-
if (value === null || value === undefined) {
119-
return '';
120-
}
121-
return value;
122-
}
123-
124-
function expandedJsonStringifyReplacer(key, value) {
125-
if (value === null || value === undefined) {
126-
return '';
127-
}
128-
116+
117+
function explainJwtClaim(claimType, value) {
129118
// Add explanations for common JWT claims using a comment-like syntax
130-
switch (key) {
119+
switch (claimType) {
131120
// header claims
132121
case 'alg':
133-
return value + ' // ' + explainAlgorithm(value) + 'Algorithm used to sign the JWT';
122+
return '// ' + explainAlgorithm(value) + 'Algorithm used to sign the JWT';
134123
case 'kid':
135-
return value + ' // Key ID, identifying which key was used to sign the JWT';
124+
return '// Key ID, identifying which key was used to sign the JWT';
136125
case 'typ':
137-
return value + ' // Type of the token, typically "JWT"';
126+
return '// Type of the token, typically "JWT"';
138127
case 'cty':
139-
return value + ' // Content type, similar to MIME type, indicating the media type of the JWT.';
128+
return '// Content type, similar to MIME type, indicating the media type of the JWT.';
140129
case 'jwk':
141-
if (typeof value === 'object') {
142-
return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // JWK, a JSON Web Key representing the public key used to verify the JWT signature';
143-
}
144-
return value + ' // JWK, a JSON Web Key representing the public key used to verify the JWT signature';
130+
return '// JWK, a JSON Web Key representing the public key used to verify the JWT signature';
145131
case 'jku':
146-
return value + ' // JWK Set URL, a URL pointing to the JSON Web Key Set containing the public key used to verify the JWT signature';
132+
return '// JWK Set URL, a URL pointing to the JSON Web Key Set containing the public key used to verify the JWT signature';
147133
case 'x5u':
148-
return value + ' // X.509 URL, a URL pointing to an X.509 certificate chain used to verify the JWT signature';
134+
return '// X.509 URL, a URL pointing to an X.509 certificate chain used to verify the JWT signature';
149135
case 'x5c':
150-
if (Array.isArray(value)) {
151-
return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // X.509 certificate chain used to verify the JWT signature';
152-
}
153-
return value + ' // X.509 certificate chain used to verify the JWT signature';
136+
return '// X.509 certificate chain used to verify the JWT signature';
154137
case 'x5t':
155-
return value + ' // X.509 certificate SHA-1 thumbprint, a hash of the X.509 certificate used to verify the JWT signature';
138+
return '// X.509 certificate SHA-1 thumbprint, a hash of the X.509 certificate used to verify the JWT signature';
156139
case 'x5t#S256':
157-
return value + ' // X.509 certificate SHA-256 thumbprint, a hash of the X.509 certificate used to verify the JWT signature';
140+
return '// X.509 certificate SHA-256 thumbprint, a hash of the X.509 certificate used to verify the JWT signature';
158141
case 'crit':
159-
if (Array.isArray(value)) {
160-
return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Critical header parameters that must be understood by the recipient';
161-
}
162-
return value + ' // Critical header parameters that must be understood by the recipient';
142+
return '// Critical header parameters that must be understood by the recipient';
163143
164144
// payload claims
165145
case 'iss':
166-
return value + ' // Issuer of the JWT, typically the authorization server';
146+
return '// Issuer of the JWT, typically the authorization server';
167147
case 'aud':
168-
return value + ' // Recipient(s) for which the JWT is intended';
148+
return '// Recipient(s) for which the JWT is intended';
169149
case 'iat':
170-
return value + ' // ' + convertEpoch(value) + 'Issued at time, in seconds since epoch';
150+
return '// ' + convertEpoch(value) + 'Issued at time, in seconds since epoch';
171151
case 'exp':
172-
return value + ' // ' + convertEpoch(value) + 'Expiration time, in seconds since epoch';
152+
return '// ' + convertEpoch(value) + 'Expiration time, in seconds since epoch';
173153
case 'nbf':
174-
return value + ' // ' + convertEpoch(value) + 'Not before time, in seconds since epoch';
154+
return '// ' + convertEpoch(value) + 'Not before time, in seconds since epoch';
175155
case 'jti':
176-
return value + ' // JWT ID, a unique identifier for the JWT';
156+
return '// JWT ID, a unique identifier for the JWT';
177157
case 'at_hash':
178-
return value + ' // Hash of the access token, used to verify the integrity of the access token';
158+
return '// Hash of the access token, used to verify the integrity of the access token';
179159
case 'c_hash':
180-
return value + ' // Hash of the authorization code, used to verify the integrity of the authorization code';
160+
return '// Hash of the authorization code, used to verify the integrity of the authorization code';
181161
case 'nonce':
182-
return value + ' // Nonce (number used only once), a unique value used to associate a client session with an ID Token, preventing replay attacks';
162+
return '// Nonce (number used only once), a unique value used to associate a client session with an ID Token, preventing replay attacks';
183163
case 'acr':
184-
return value + ' // Authentication Context Class Reference, indicating the authentication method used';
164+
return '// Authentication Context Class Reference, indicating the authentication method used';
185165
case 'amr':
186-
if (Array.isArray(value)) {
187-
return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Authentication Methods References, indicating the methods used for authentication';
188-
}
189-
return value + ' // Authentication Methods References, indicating the methods used for authentication';
166+
return '// Authentication Methods References, indicating the methods used for authentication';
190167
191168
// OIDC claims
192169
case 'sub':
193-
return value + ' // Subject identifier';
170+
return '// Subject identifier';
194171
case 'name':
195-
return value + ' // Display name of the user';
172+
return '// Display name of the user';
196173
case 'given_name':
197-
return value + ' // Given name(s) or first name(s) of the user';
174+
return '// Given name(s) or first name(s) of the user';
198175
case 'family_name':
199-
return value + ' // Surname(s) or last name(s) of the user';
176+
return '// Surname(s) or last name(s) of the user';
200177
case 'middle_name':
201-
return value + ' // Middle name(s) of the user';
178+
return '// Middle name(s) of the user';
202179
case 'nickname':
203-
return value + ' // Casual name of the user';
180+
return '// Casual name of the user';
204181
case 'preferred_username':
205-
return value + ' // Preferred username of the user, often used for login';
182+
return '// Preferred username of the user, often used for login';
206183
case 'birthdate':
207-
return value + ' // Birthdate of the user, typically in ISO 8601 format (YYYY-MM-DD)';
184+
return '// Birthdate of the user, typically in ISO 8601 format (YYYY-MM-DD)';
208185
case 'gender':
209-
return value + ' // Gender of the user';
186+
return '// Gender of the user';
210187
case 'email':
211-
return value + ' // Email address of the user';
188+
return '// Email address of the user';
212189
case 'email_verified':
213-
return value + ' // Indicates whether the email address has been verified (true/false)';
190+
return '// Indicates whether the email address has been verified (true/false)';
214191
case 'phone_number':
215-
return value + ' // Phone number of the user';
192+
return '// Phone number of the user';
216193
case 'phone_number_verified':
217-
return value + ' // Indicates whether the phone number has been verified (true/false)';
194+
return '// Indicates whether the phone number has been verified (true/false)';
218195
case 'address':
219-
if (typeof value === 'object') {
220-
return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Address of the user, typically an object with street, city, state, postal code, and country';
221-
}
222-
return value + ' // Address of the user, typically an object with street, city, state, postal code, and country';
196+
return '// Address of the user, typically an object with street, city, state, postal code, and country';
223197
case 'locale':
224-
return value + ' // Locale of the user, typically a language code like "en-US"';
198+
return '// Locale of the user, typically a language code like "en-US"';
225199
case 'zoneinfo':
226-
return value + ' // Time zone of the user, typically a string like "America/New_York"';
200+
return '// Time zone of the user, typically a string like "America/New_York"';
227201
case 'profile':
228-
return value + ' // URL of the user\'s profile, often a link to their social media or personal page';
202+
return '// URL of the user\'s profile, often a link to their social media or personal page';
229203
case 'picture':
230-
return value + ' // URL of the user\'s profile picture';
204+
return '// URL of the user\'s profile picture';
231205
case 'website':
232-
return value + ' // URL of the user\'s personal website or profile';
206+
return '// URL of the user\'s personal website or profile';
233207
}
234208
235-
return value;
209+
return undefined;
236210
}
237211
238212
function explainAlgorithm(alg) {
@@ -271,14 +245,12 @@
271245
function convertEpoch(epoch) {
272246
if (typeof epoch === 'number') {
273247
const date = new Date(epoch * 1000);
274-
return date.toISOString() + '. ';
248+
return date.toLocaleString() + '. ';
275249
}
276250
277251
return '';
278252
}
279253
280-
let jsonStringifyReplacer = defaultJsonStringifyReplacer;
281-
282254
$(document).ready(initializeJwtDecoder);
283255
284256
function readSettingsFromStorage() {
@@ -290,7 +262,6 @@
290262
291263
$('#explainClaims').prop('checked', options.explainClaims);
292264
$('#togglePresenterMode').prop('checked', options.presenterMode);
293-
jsonStringifyReplacer = options.explainClaims ? expandedJsonStringifyReplacer : defaultJsonStringifyReplacer;
294265
setPresenterMode(options.presenterMode);
295266
}
296267
catch (e) {}
@@ -322,7 +293,7 @@
322293
$('#explainClaims').on('change', async function() {
323294
options.explainClaims = this.checked;
324295
await saveSettingsToStorage();
325-
jsonStringifyReplacer = options.explainClaims ? expandedJsonStringifyReplacer : defaultJsonStringifyReplacer;
296+
326297
updateClaimsExplanation();
327298
});
328299
@@ -400,24 +371,117 @@
400371
clearError();
401372
hideSignatureValidationResults();
402373
403-
$('#jwt-header').text(header ? JSON.stringify(header, jsonStringifyReplacer, 2) : ' ');
404-
$('#jwt-payload').text(payload ? JSON.stringify(payload, jsonStringifyReplacer, 2) : ' ');
374+
writeJson($('#jwt-header'), header);
375+
writeJson($('#jwt-payload'), payload);
376+
405377
$('#jwt-signature').text(signature || ' ');
406378
407379
if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) {
408380
await attemptSignatureValidation(header, payload, jwtParts);
409381
}
410382
}
411-
383+
412384
function updateClaimsExplanation() {
413385
if (!decodedJwt.payload) {
414386
return;
415387
}
416388
417-
$('#jwt-header').text(decodedJwt.header ? JSON.stringify(decodedJwt.header, jsonStringifyReplacer, 2) : ' ');
418-
$('#jwt-payload').text(decodedJwt.payload ? JSON.stringify(decodedJwt.payload, jsonStringifyReplacer, 2) : ' ');
389+
writeJson($('#jwt-header'), decodedJwt.header);
390+
writeJson($('#jwt-payload'), decodedJwt.payload);
391+
}
392+
393+
function writeJson(target, data) {
394+
if (data === null || data === undefined || !data) {
395+
target.html('&nbsp;'); // Empty space
396+
return;
397+
}
398+
399+
try {
400+
const jsonContent = buildJsonHtmlContent(data);
401+
target.empty();
402+
target.append(jsonContent);
403+
} catch (e) {
404+
target.html('&nbsp;'); // Empty space
405+
showError('Error formatting JSON: ' + e.message);
406+
}
407+
}
408+
409+
function buildJsonHtmlContent(data) {
410+
const jsonContent = $('<div class="json-content"></div>');
411+
412+
if (Array.isArray(data)) {
413+
jsonContent.append('<span class="json-array-begin">[</span>');
414+
data.forEach((item, index, arr) => {
415+
const itemDiv = $(`<div class="json-item" style="padding-left: 1em"></div>`);
416+
itemDiv.append(buildJsonValueHtml(item, null, index === arr.length - 1, false));
417+
jsonContent.append(itemDiv);
418+
});
419+
jsonContent.append('<span class="json-array-end">]</span>');
420+
} else if (typeof data === 'object') {
421+
jsonContent.append('<span class="json-object-begin">{</span>');
422+
Object.keys(data).forEach((key, index, arr) => {
423+
const itemDiv = $(`<div class="json-item" style="padding-left: 1em"></div>`);
424+
itemDiv.append(`<span class="json-key">"${key}"</span>: `);
425+
itemDiv.append(buildJsonValueHtml(data[key], key, index === arr.length - 1, true));
426+
jsonContent.append(itemDiv);
427+
});
428+
jsonContent.append('<span class="json-object-end">}</span>');
429+
} else {
430+
jsonContent.text(data);
431+
}
432+
433+
return jsonContent;
419434
}
420435
436+
function buildJsonValueHtml(value, key, isLastItem = false, explain = true) {
437+
let contents = $('<span class="json-value"></span>');
438+
439+
if (Array.isArray(value)) {
440+
contents = $('<span class="json-array-begin">[</span>');
441+
value.forEach((item, index, arr) => {
442+
const itemDiv = $(`<div class="json-item" style="padding-left: 1em"></div>`);
443+
itemDiv.append(buildJsonValueHtml(item, null, index === arr.length - 1, false));
444+
contents.append(itemDiv);
445+
});
446+
contents.append(`<span class="json-array-end">]${(isLastItem ? '' : ',')}</span>`);
447+
}
448+
else if (typeof value === 'object') {
449+
contents = $('<span class="json-object-begin">{</span>');
450+
Object.keys(value).forEach((key, index, arr) => {
451+
const itemDiv = $('<div class="json-item" style="padding-left: 1em"></div>');
452+
itemDiv.append(`<span class="json-key">"${key}"</span>: `);
453+
itemDiv.append(buildJsonValueHtml(value[key], key, index === arr.length - 1, false)); // disable explanation for nested objects
454+
contents.append(itemDiv);
455+
});
456+
contents.append(`<span class="json-object-end">}${(isLastItem ? '' : ',')}</span>`);
457+
} else if (typeof value === 'string') {
458+
const text = JSON.stringify(value) + (isLastItem ? '' : ',');
459+
contents.text(text);
460+
contents.addClass('json-string');
461+
} else if (typeof value === 'number') {
462+
const text = value + (isLastItem ? '' : ',');
463+
contents.text(text);
464+
contents.addClass('json-number');
465+
} else if (typeof value === 'boolean') {
466+
const text = (value ? 'true' : 'false') + (isLastItem ? '' : ',');
467+
contents.text(text);
468+
contents.addClass('json-boolean');
469+
} else {
470+
const text = String(value) + (isLastItem ? '' : ',');
471+
contents.text(text);
472+
contents.addClass('json-unknown');
473+
}
474+
475+
if (options.explainClaims && key && explain) {
476+
const explanation = explainJwtClaim(key, value);
477+
if (explanation) {
478+
contents.append(`<span class="json-explanation"> ${explanation}</span>`);
479+
}
480+
}
481+
482+
return contents;
483+
}
484+
421485
async function attemptSignatureValidation(header, payload, jwtParts) {
422486
const jwksUrlField = $('#jwks-url');
423487

0 commit comments

Comments
 (0)