|
1 |
| -@page "~/jwt-decoder" |
| 1 | +@page "~/jwt-decoder" |
2 | 2 | @model IdentityServerHost.Pages.Home.JwtDecoder
|
3 | 3 |
|
4 | 4 | <!-- Google Tag Manager (noscript) -->
|
|
113 | 113 | presenterMode: false,
|
114 | 114 | explainClaims: false
|
115 | 115 | };
|
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) { |
129 | 118 | // Add explanations for common JWT claims using a comment-like syntax
|
130 |
| - switch (key) { |
| 119 | + switch (claimType) { |
131 | 120 | // header claims
|
132 | 121 | case 'alg':
|
133 |
| - return value + ' // ' + explainAlgorithm(value) + 'Algorithm used to sign the JWT'; |
| 122 | + return '// ' + explainAlgorithm(value) + 'Algorithm used to sign the JWT'; |
134 | 123 | 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'; |
136 | 125 | case 'typ':
|
137 |
| - return value + ' // Type of the token, typically "JWT"'; |
| 126 | + return '// Type of the token, typically "JWT"'; |
138 | 127 | 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.'; |
140 | 129 | 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'; |
145 | 131 | 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'; |
147 | 133 | 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'; |
149 | 135 | 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'; |
154 | 137 | 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'; |
156 | 139 | 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'; |
158 | 141 | 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'; |
163 | 143 |
|
164 | 144 | // payload claims
|
165 | 145 | case 'iss':
|
166 |
| - return value + ' // Issuer of the JWT, typically the authorization server'; |
| 146 | + return '// Issuer of the JWT, typically the authorization server'; |
167 | 147 | case 'aud':
|
168 |
| - return value + ' // Recipient(s) for which the JWT is intended'; |
| 148 | + return '// Recipient(s) for which the JWT is intended'; |
169 | 149 | 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'; |
171 | 151 | case 'exp':
|
172 |
| - return value + ' // ' + convertEpoch(value) + 'Expiration time, in seconds since epoch'; |
| 152 | + return '// ' + convertEpoch(value) + 'Expiration time, in seconds since epoch'; |
173 | 153 | 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'; |
175 | 155 | case 'jti':
|
176 |
| - return value + ' // JWT ID, a unique identifier for the JWT'; |
| 156 | + return '// JWT ID, a unique identifier for the JWT'; |
177 | 157 | 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'; |
179 | 159 | 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'; |
181 | 161 | 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'; |
183 | 163 | 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'; |
185 | 165 | 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'; |
190 | 167 |
|
191 | 168 | // OIDC claims
|
192 | 169 | case 'sub':
|
193 |
| - return value + ' // Subject identifier'; |
| 170 | + return '// Subject identifier'; |
194 | 171 | case 'name':
|
195 |
| - return value + ' // Display name of the user'; |
| 172 | + return '// Display name of the user'; |
196 | 173 | 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'; |
198 | 175 | 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'; |
200 | 177 | case 'middle_name':
|
201 |
| - return value + ' // Middle name(s) of the user'; |
| 178 | + return '// Middle name(s) of the user'; |
202 | 179 | case 'nickname':
|
203 |
| - return value + ' // Casual name of the user'; |
| 180 | + return '// Casual name of the user'; |
204 | 181 | 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'; |
206 | 183 | 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)'; |
208 | 185 | case 'gender':
|
209 |
| - return value + ' // Gender of the user'; |
| 186 | + return '// Gender of the user'; |
210 | 187 | case 'email':
|
211 |
| - return value + ' // Email address of the user'; |
| 188 | + return '// Email address of the user'; |
212 | 189 | 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)'; |
214 | 191 | case 'phone_number':
|
215 |
| - return value + ' // Phone number of the user'; |
| 192 | + return '// Phone number of the user'; |
216 | 193 | 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)'; |
218 | 195 | 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'; |
223 | 197 | 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"'; |
225 | 199 | 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"'; |
227 | 201 | 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'; |
229 | 203 | case 'picture':
|
230 |
| - return value + ' // URL of the user\'s profile picture'; |
| 204 | + return '// URL of the user\'s profile picture'; |
231 | 205 | 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'; |
233 | 207 | }
|
234 | 208 |
|
235 |
| - return value; |
| 209 | + return undefined; |
236 | 210 | }
|
237 | 211 |
|
238 | 212 | function explainAlgorithm(alg) {
|
|
271 | 245 | function convertEpoch(epoch) {
|
272 | 246 | if (typeof epoch === 'number') {
|
273 | 247 | const date = new Date(epoch * 1000);
|
274 |
| - return date.toISOString() + '. '; |
| 248 | + return date.toLocaleString() + '. '; |
275 | 249 | }
|
276 | 250 |
|
277 | 251 | return '';
|
278 | 252 | }
|
279 | 253 |
|
280 |
| - let jsonStringifyReplacer = defaultJsonStringifyReplacer; |
281 |
| - |
282 | 254 | $(document).ready(initializeJwtDecoder);
|
283 | 255 |
|
284 | 256 | function readSettingsFromStorage() {
|
|
290 | 262 |
|
291 | 263 | $('#explainClaims').prop('checked', options.explainClaims);
|
292 | 264 | $('#togglePresenterMode').prop('checked', options.presenterMode);
|
293 |
| - jsonStringifyReplacer = options.explainClaims ? expandedJsonStringifyReplacer : defaultJsonStringifyReplacer; |
294 | 265 | setPresenterMode(options.presenterMode);
|
295 | 266 | }
|
296 | 267 | catch (e) {}
|
|
322 | 293 | $('#explainClaims').on('change', async function() {
|
323 | 294 | options.explainClaims = this.checked;
|
324 | 295 | await saveSettingsToStorage();
|
325 |
| - jsonStringifyReplacer = options.explainClaims ? expandedJsonStringifyReplacer : defaultJsonStringifyReplacer; |
| 296 | + |
326 | 297 | updateClaimsExplanation();
|
327 | 298 | });
|
328 | 299 |
|
|
400 | 371 | clearError();
|
401 | 372 | hideSignatureValidationResults();
|
402 | 373 |
|
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 | + |
405 | 377 | $('#jwt-signature').text(signature || ' ');
|
406 | 378 |
|
407 | 379 | if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) {
|
408 | 380 | await attemptSignatureValidation(header, payload, jwtParts);
|
409 | 381 | }
|
410 | 382 | }
|
411 |
| - |
| 383 | +
|
412 | 384 | function updateClaimsExplanation() {
|
413 | 385 | if (!decodedJwt.payload) {
|
414 | 386 | return;
|
415 | 387 | }
|
416 | 388 |
|
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(' '); // 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(' '); // 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; |
419 | 434 | }
|
420 | 435 |
|
| 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 | + |
421 | 485 | async function attemptSignatureValidation(header, payload, jwtParts) {
|
422 | 486 | const jwksUrlField = $('#jwks-url');
|
423 | 487 |
|
|
0 commit comments