|
23 | 23 | Always be cautious when pasting JWTs, as they may contain credentials or sensitive information.
|
24 | 24 | </div>
|
25 | 25 | </div>
|
26 |
| - <div class="jwt-decoder-container container"> |
| 26 | + <div class="jwt-decoder-container container-fluid container-lg"> |
27 | 27 | <div class="row align-items-stretch">
|
28 |
| - <div class="col-6"> |
| 28 | + <div class="col-12 col-md-6 pb-4 pb-md-0"> |
29 | 29 | <h3>Encoded JWT</h3>
|
30 | 30 | <div class="h-100 d-flex flex-column">
|
31 | 31 | <div class="form-group flex-grow-1">
|
32 | 32 | <label for="jwt-input" class="sr-only">Paste your JWT here...</label>
|
33 |
| - <textarea id="jwt-input" class="form-control bg-dark text-light p-2 h-100" rows="8" cols="70" placeholder="Paste your JWT here..."></textarea> |
| 33 | + <div id="jwt-input" class="form-control bg-dark text-light p-2 h-100 jwt-input-editable" contenteditable="true" rows="8" style="min-height: 10em;" placeholder="Paste your JWT here..."></div> |
34 | 34 | </div>
|
35 | 35 | <div class="form-group">
|
36 | 36 | <label for="jwks-url">Issuer, Discovery Document or JWKs URI</label>
|
37 |
| - <input type="url" class="form-control mb-2 mr-sm-2" id="jwks-url" name="jwks-url" value="https://demo.duendesoftware.com" aria-describedby="jwks-url-help" /> |
| 37 | + <input type="url" class="form-control mb-2 mr-sm-2" id="jwks-url" name="jwks-url" aria-describedby="jwks-url-help" /> |
38 | 38 | <small id="jwks-url-help" class="form-text text-muted">
|
39 | 39 | Optionally, you can provide the issuer, discovery document or JWKs URI to validate the JWT's signature.
|
40 |
| - If you leave this field empty, the JWT will only be decoded without validation. |
| 40 | + If you leave this field empty, the tool will use the value of the 'iss' claim. |
41 | 41 | </small>
|
42 | 42 | </div>
|
43 | 43 | </div>
|
44 | 44 | </div>
|
45 |
| - <div class="col-6"> |
| 45 | + <div class="col-12 col-md-6 pb-4 pb-md-0"> |
46 | 46 | <h3>Decoded JWT</h3>
|
| 47 | + <div class="form-check"> |
| 48 | + <input class="form-check-input" type="checkbox" value="1" id="explainClaims"> |
| 49 | + <label class="form-check-label" for="explainClaims"> |
| 50 | + Show claim information |
| 51 | + </label> |
| 52 | + </div> |
47 | 53 | <div class="jwt-decoded-output">
|
48 | 54 | <h4>Header</h4>
|
49 |
| - <pre id="jwt-header" class="bg-dark text-light p-2"> </pre> |
| 55 | + <pre id="jwt-header" class="jwt-decoded jwt-header bg-dark p-2"> </pre> |
50 | 56 | <h4>Payload</h4>
|
51 |
| - <pre id="jwt-payload" class="bg-dark text-light p-2"> </pre> |
| 57 | + <pre id="jwt-payload" class="jwt-decoded jwt-payload bg-dark p-2"> </pre> |
52 | 58 | <h4>Signature</h4>
|
53 |
| - <pre id="jwt-signature" class="bg-dark text-light p-2"> </pre> |
| 59 | + <pre id="jwt-signature" class="jwt-decoded jwt-signature bg-dark p-2"> </pre> |
54 | 60 | <div class="jwt-signature-validation-result alert alert-success align-items-center d-none">
|
55 | 61 | <i class="glyphicon glyphicon-ok-sign" style="font-size: 3em" title="Valid signature"></i>
|
56 | 62 | <div class="result-message mx-3">This JWT has a valid signature.</div>
|
|
88 | 94 | loadedFrom: null
|
89 | 95 | };
|
90 | 96 |
|
| 97 | + let decodedJwt = { |
| 98 | + header: null, |
| 99 | + payload: null, |
| 100 | + signature: null |
| 101 | + }; |
| 102 | + |
| 103 | + let explainClaims = false; |
| 104 | + |
| 105 | + function defaultJsonStringifyReplacer(key, value) { |
| 106 | + if (value === null || value === undefined) { |
| 107 | + return ''; |
| 108 | + } |
| 109 | + return value; |
| 110 | + } |
| 111 | + |
| 112 | + function expandedJsonStringifyReplacer(key, value) { |
| 113 | + if (value === null || value === undefined) { |
| 114 | + return ''; |
| 115 | + } |
| 116 | + |
| 117 | + // Add explanations for common JWT claims using a comment-like syntax |
| 118 | + switch (key) { |
| 119 | + // header claims |
| 120 | + case 'alg': |
| 121 | + return value + ' // ' + explainAlgorithm(value) + 'Algorithm used to sign the JWT'; |
| 122 | + case 'kid': |
| 123 | + return value + ' // Key ID, identifying which key was used to sign the JWT'; |
| 124 | + case 'typ': |
| 125 | + return value + ' // Type of the token, typically "JWT"'; |
| 126 | + case 'cty': |
| 127 | + return value + ' // Content type, similar to MIME type, indicating the media type of the JWT.'; |
| 128 | + case 'jwk': |
| 129 | + if (typeof value === 'object') { |
| 130 | + return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // JWK, a JSON Web Key representing the public key used to verify the JWT signature'; |
| 131 | + } |
| 132 | + return value + ' // JWK, a JSON Web Key representing the public key used to verify the JWT signature'; |
| 133 | + case 'jku': |
| 134 | + return value + ' // JWK Set URL, a URL pointing to the JSON Web Key Set containing the public key used to verify the JWT signature'; |
| 135 | + case 'x5u': |
| 136 | + return value + ' // X.509 URL, a URL pointing to an X.509 certificate chain used to verify the JWT signature'; |
| 137 | + case 'x5c': |
| 138 | + if (Array.isArray(value)) { |
| 139 | + return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // X.509 certificate chain used to verify the JWT signature'; |
| 140 | + } |
| 141 | + return value + ' // X.509 certificate chain used to verify the JWT signature'; |
| 142 | + case 'x5t': |
| 143 | + return value + ' // X.509 certificate SHA-1 thumbprint, a hash of the X.509 certificate used to verify the JWT signature'; |
| 144 | + case 'x5t#S256': |
| 145 | + return value + ' // X.509 certificate SHA-256 thumbprint, a hash of the X.509 certificate used to verify the JWT signature'; |
| 146 | + case 'crit': |
| 147 | + if (Array.isArray(value)) { |
| 148 | + return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Critical header parameters that must be understood by the recipient'; |
| 149 | + } |
| 150 | + return value + ' // Critical header parameters that must be understood by the recipient'; |
| 151 | + |
| 152 | + // payload claims |
| 153 | + case 'iss': |
| 154 | + return value + ' // Issuer of the JWT, typically the authorization server'; |
| 155 | + case 'aud': |
| 156 | + return value + ' // Recipient(s) for which the JWT is intended'; |
| 157 | + case 'iat': |
| 158 | + return value + ' // ' + convertEpoch(value) + 'Issued at time, in seconds since epoch'; |
| 159 | + case 'exp': |
| 160 | + return value + ' // ' + convertEpoch(value) + 'Expiration time, in seconds since epoch'; |
| 161 | + case 'nbf': |
| 162 | + return value + ' // ' + convertEpoch(value) + 'Not before time, in seconds since epoch'; |
| 163 | + case 'jti': |
| 164 | + return value + ' // JWT ID, a unique identifier for the JWT'; |
| 165 | + case 'at_hash': |
| 166 | + return value + ' // Hash of the access token, used to verify the integrity of the access token'; |
| 167 | + case 'c_hash': |
| 168 | + return value + ' // Hash of the authorization code, used to verify the integrity of the authorization code'; |
| 169 | + case 'nonce': |
| 170 | + return value + ' // Nonce (number used only once), a unique value used to associate a client session with an ID Token, preventing replay attacks'; |
| 171 | + case 'acr': |
| 172 | + return value + ' // Authentication Context Class Reference, indicating the authentication method used'; |
| 173 | + case 'amr': |
| 174 | + if (Array.isArray(value)) { |
| 175 | + return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Authentication Methods References, indicating the methods used for authentication'; |
| 176 | + } |
| 177 | + return value + ' // Authentication Methods References, indicating the methods used for authentication'; |
| 178 | + |
| 179 | + // OIDC claims |
| 180 | + case 'sub': |
| 181 | + return value + ' // Subject identifier'; |
| 182 | + case 'name': |
| 183 | + return value + ' // Display name of the user'; |
| 184 | + case 'given_name': |
| 185 | + return value + ' // Given name(s) or first name(s) of the user'; |
| 186 | + case 'family_name': |
| 187 | + return value + ' // Surname(s) or last name(s) of the user'; |
| 188 | + case 'middle_name': |
| 189 | + return value + ' // Middle name(s) of the user'; |
| 190 | + case 'nickname': |
| 191 | + return value + ' // Casual name of the user'; |
| 192 | + case 'preferred_username': |
| 193 | + return value + ' // Preferred username of the user, often used for login'; |
| 194 | + case 'birthdate': |
| 195 | + return value + ' // Birthdate of the user, typically in ISO 8601 format (YYYY-MM-DD)'; |
| 196 | + case 'gender': |
| 197 | + return value + ' // Gender of the user'; |
| 198 | + case 'email': |
| 199 | + return value + ' // Email address of the user'; |
| 200 | + case 'email_verified': |
| 201 | + return value + ' // Indicates whether the email address has been verified (true/false)'; |
| 202 | + case 'phone_number': |
| 203 | + return value + ' // Phone number of the user'; |
| 204 | + case 'phone_number_verified': |
| 205 | + return value + ' // Indicates whether the phone number has been verified (true/false)'; |
| 206 | + case 'address': |
| 207 | + if (typeof value === 'object') { |
| 208 | + return JSON.stringify(value, defaultJsonStringifyReplacer) + ' // Address of the user, typically an object with street, city, state, postal code, and country'; |
| 209 | + } |
| 210 | + return value + ' // Address of the user, typically an object with street, city, state, postal code, and country'; |
| 211 | + case 'locale': |
| 212 | + return value + ' // Locale of the user, typically a language code like "en-US"'; |
| 213 | + case 'zoneinfo': |
| 214 | + return value + ' // Time zone of the user, typically a string like "America/New_York"'; |
| 215 | + case 'profile': |
| 216 | + return value + ' // URL of the user\'s profile, often a link to their social media or personal page'; |
| 217 | + case 'picture': |
| 218 | + return value + ' // URL of the user\'s profile picture'; |
| 219 | + case 'website': |
| 220 | + return value + ' // URL of the user\'s personal website or profile'; |
| 221 | + } |
| 222 | + |
| 223 | + return value; |
| 224 | + } |
| 225 | + |
| 226 | + function explainAlgorithm(alg) { |
| 227 | + switch (alg) { |
| 228 | + case 'RS256': |
| 229 | + return 'RSA with SHA-256. '; |
| 230 | + case 'RS384': |
| 231 | + return 'RSA with SHA-384. '; |
| 232 | + case 'RS512': |
| 233 | + return 'RSA with SHA-512. '; |
| 234 | + case 'ES256': |
| 235 | + return 'ECDSA with P-256 curve. '; |
| 236 | + case 'ES384': |
| 237 | + return 'ECDSA with P-384 curve. '; |
| 238 | + case 'ES512': |
| 239 | + return 'ECDSA with P-521 curve. '; |
| 240 | + case 'PS256': |
| 241 | + return 'RSA-PSS with SHA-256. '; |
| 242 | + case 'PS384': |
| 243 | + return 'RSA-PSS with SHA-384. '; |
| 244 | + case 'PS512': |
| 245 | + return 'RSA-PSS with SHA-512. '; |
| 246 | + case 'HS256': |
| 247 | + return 'HMAC with SHA-256 shared secret. '; |
| 248 | + case 'HS384': |
| 249 | + return 'HMAC with SHA-384 shared secret. '; |
| 250 | + case 'HS512': |
| 251 | + return 'HMAC with SHA-512 shared secret. '; |
| 252 | + case 'none': |
| 253 | + return 'No signature algorithm. This JWT is unsigned and should not be trusted. '; |
| 254 | + default: |
| 255 | + return 'Unknown algorithm. '; |
| 256 | + } |
| 257 | + } |
| 258 | + |
| 259 | + function convertEpoch(epoch) { |
| 260 | + if (typeof epoch === 'number') { |
| 261 | + const date = new Date(epoch * 1000); |
| 262 | + return date.toISOString() + '. '; |
| 263 | + } |
| 264 | + |
| 265 | + return ''; |
| 266 | + } |
| 267 | + |
| 268 | + let jsonStringifyReplacer = defaultJsonStringifyReplacer; |
| 269 | + |
91 | 270 | $(document).ready(initializeJwtDecoder);
|
92 | 271 |
|
93 | 272 | async function initializeJwtDecoder() {
|
| 273 | + const explainClaimsCheckbox = $('#explainClaims'); |
| 274 | + explainClaims = explainClaimsCheckbox.is(':checked'); |
| 275 | + explainClaimsCheckbox.on('change', function() { |
| 276 | + explainClaims = this.checked; |
| 277 | + jsonStringifyReplacer = explainClaims ? expandedJsonStringifyReplacer : defaultJsonStringifyReplacer; |
| 278 | + updateClaimsExplanation(); |
| 279 | + }); |
| 280 | + |
94 | 281 | $('#jwt-input').on('input', async function() {
|
95 |
| - const jwt = $(this).val(); |
| 282 | + decodedJwt = { |
| 283 | + header: null, |
| 284 | + payload: null, |
| 285 | + signature: null |
| 286 | + }; |
| 287 | + |
| 288 | + const jwt = $(this).text(); |
96 | 289 | if (jwt) {
|
97 | 290 | try {
|
98 | 291 | const parts = jwt.split('.');
|
|
101 | 294 | const payload = JSON.parse(atob(parts[1]));
|
102 | 295 | const signature = parts[2];
|
103 | 296 |
|
| 297 | + decodedJwt = { |
| 298 | + header: header, |
| 299 | + payload: payload, |
| 300 | + signature: signature |
| 301 | + }; |
104 | 302 | await showDecodedJwt(parts, header, payload, signature);
|
| 303 | + colorJwtInput($(this), parts); |
105 | 304 | } else {
|
106 | 305 | showError('Invalid JWT format. A JWT should have three parts separated by dots.');
|
107 | 306 | }
|
|
115 | 314 |
|
116 | 315 | await showDecodedJwt(null, null, null, ' ');
|
117 | 316 | }
|
| 317 | +
|
| 318 | + function colorJwtInput(target, parts) { |
| 319 | + let html = ''; |
| 320 | + if (parts.length > 0) { |
| 321 | + html += `<span class="text-danger">${parts[0] || ''}</span>`; |
| 322 | + } |
| 323 | + if (parts.length > 1) { |
| 324 | + html += `<span class="text-success">.${parts[1] || ''}</span>`; |
| 325 | + } |
| 326 | + if (parts.length > 2) { |
| 327 | + html += `<span class="text-warning">.${parts.slice(2).join('.') || ''}</span>`; |
| 328 | + } |
| 329 | + target.html(html); |
| 330 | + } |
118 | 331 |
|
119 | 332 | async function showDecodedJwt(jwtParts, header, payload, signature) {
|
120 | 333 | clearError();
|
121 | 334 | hideSignatureValidationResults();
|
122 | 335 |
|
123 |
| - $('#jwt-header').text(header ? JSON.stringify(header, null, 2) : ' '); |
124 |
| - $('#jwt-payload').text(payload ? JSON.stringify(payload, null, 2) : ' '); |
| 336 | + $('#jwt-header').text(header ? JSON.stringify(header, jsonStringifyReplacer, 2) : ' '); |
| 337 | + $('#jwt-payload').text(payload ? JSON.stringify(payload, jsonStringifyReplacer, 2) : ' '); |
125 | 338 | $('#jwt-signature').text(signature || ' ');
|
126 | 339 |
|
127 | 340 | if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) {
|
128 |
| - await attemptSignatureValidation(header, jwtParts); |
| 341 | + await attemptSignatureValidation(header, payload, jwtParts); |
129 | 342 | }
|
130 | 343 | }
|
131 | 344 |
|
132 |
| - async function attemptSignatureValidation(header, jwtParts) { |
133 |
| - const jwksUrl = $('#jwks-url').val().trim(); |
| 345 | + function updateClaimsExplanation() { |
| 346 | + if (!decodedJwt.payload) { |
| 347 | + return; |
| 348 | + } |
| 349 | +
|
| 350 | + $('#jwt-header').text(decodedJwt.header ? JSON.stringify(decodedJwt.header, jsonStringifyReplacer, 2) : ' '); |
| 351 | + $('#jwt-payload').text(decodedJwt.payload ? JSON.stringify(decodedJwt.payload, jsonStringifyReplacer, 2) : ' '); |
| 352 | + } |
| 353 | + |
| 354 | + async function attemptSignatureValidation(header, payload, jwtParts) { |
| 355 | + let jwksUrl = $('#jwks-url').val().trim(); |
| 356 | + if (!jwksUrl && payload && payload.iss) { |
| 357 | + // If no JWKs URL is provided, use the issuer from the payload. |
| 358 | + jwksUrl = payload.iss; |
| 359 | + $('#jwks-url').val(jwksUrl); |
| 360 | + } |
134 | 361 |
|
135 | 362 | if (jwksUrl) {
|
136 | 363 | await loadJwks(jwksUrl);
|
|
0 commit comments