Skip to content

Commit 62b8278

Browse files
committed
Added JWT coloring, claims explanation and signature validation using iss claim
1 parent 214c0cb commit 62b8278

File tree

5 files changed

+272
-17
lines changed

5 files changed

+272
-17
lines changed

src/Pages/Home/JwtDecoder/JwtDecoder.cshtml

Lines changed: 242 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,34 +23,40 @@
2323
Always be cautious when pasting JWTs, as they may contain credentials or sensitive information.
2424
</div>
2525
</div>
26-
<div class="jwt-decoder-container container">
26+
<div class="jwt-decoder-container container-fluid container-lg">
2727
<div class="row align-items-stretch">
28-
<div class="col-6">
28+
<div class="col-12 col-md-6 pb-4 pb-md-0">
2929
<h3>Encoded JWT</h3>
3030
<div class="h-100 d-flex flex-column">
3131
<div class="form-group flex-grow-1">
3232
<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>
3434
</div>
3535
<div class="form-group">
3636
<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" />
3838
<small id="jwks-url-help" class="form-text text-muted">
3939
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.
4141
</small>
4242
</div>
4343
</div>
4444
</div>
45-
<div class="col-6">
45+
<div class="col-12 col-md-6 pb-4 pb-md-0">
4646
<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>
4753
<div class="jwt-decoded-output">
4854
<h4>Header</h4>
49-
<pre id="jwt-header" class="bg-dark text-light p-2">&nbsp;</pre>
55+
<pre id="jwt-header" class="jwt-decoded jwt-header bg-dark p-2">&nbsp;</pre>
5056
<h4>Payload</h4>
51-
<pre id="jwt-payload" class="bg-dark text-light p-2">&nbsp;</pre>
57+
<pre id="jwt-payload" class="jwt-decoded jwt-payload bg-dark p-2">&nbsp;</pre>
5258
<h4>Signature</h4>
53-
<pre id="jwt-signature" class="bg-dark text-light p-2">&nbsp;</pre>
59+
<pre id="jwt-signature" class="jwt-decoded jwt-signature bg-dark p-2">&nbsp;</pre>
5460
<div class="jwt-signature-validation-result alert alert-success align-items-center d-none">
5561
<i class="glyphicon glyphicon-ok-sign" style="font-size: 3em" title="Valid signature"></i>
5662
<div class="result-message mx-3">This JWT has a valid signature.</div>
@@ -88,11 +94,198 @@
8894
loadedFrom: null
8995
};
9096
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+
91270
$(document).ready(initializeJwtDecoder);
92271
93272
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+
94281
$('#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();
96289
if (jwt) {
97290
try {
98291
const parts = jwt.split('.');
@@ -101,7 +294,13 @@
101294
const payload = JSON.parse(atob(parts[1]));
102295
const signature = parts[2];
103296
297+
decodedJwt = {
298+
header: header,
299+
payload: payload,
300+
signature: signature
301+
};
104302
await showDecodedJwt(parts, header, payload, signature);
303+
colorJwtInput($(this), parts);
105304
} else {
106305
showError('Invalid JWT format. A JWT should have three parts separated by dots.');
107306
}
@@ -115,22 +314,50 @@
115314
116315
await showDecodedJwt(null, null, null, ' ');
117316
}
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+
}
118331
119332
async function showDecodedJwt(jwtParts, header, payload, signature) {
120333
clearError();
121334
hideSignatureValidationResults();
122335
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) : ' ');
125338
$('#jwt-signature').text(signature || ' ');
126339
127340
if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) {
128-
await attemptSignatureValidation(header, jwtParts);
341+
await attemptSignatureValidation(header, payload, jwtParts);
129342
}
130343
}
131344
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+
}
134361
135362
if (jwksUrl) {
136363
await loadJwks(jwksUrl);

src/Pages/Shared/_Layout.cshtml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<body>
2323
<partial name="_Nav" />
2424

25-
<div class="container body-container">
25+
<div class="container-fluid container-lg body-container">
2626
@RenderBody()
2727
</div>
2828

src/wwwroot/css/site.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,3 +111,15 @@ a.navbar-brand .icon-banner {
111111
.jwt-decoder-container .row {
112112
height: 40vh;
113113
}
114+
.jwt-decoder-container .jwt-decoded {
115+
text-wrap: auto;
116+
}
117+
.jwt-decoder-container .jwt-decoded.jwt-header {
118+
color: #fc3939;
119+
}
120+
.jwt-decoder-container .jwt-decoded.jwt-payload {
121+
color: #13b955;
122+
}
123+
.jwt-decoder-container .jwt-decoded.jwt-signature {
124+
color: #efa31d;
125+
}

0 commit comments

Comments
 (0)