Skip to content

Commit 214c0cb

Browse files
committed
Got signature validation working for asymmetric algorithms
1 parent 126b5c9 commit 214c0cb

File tree

4 files changed

+136
-10
lines changed

4 files changed

+136
-10
lines changed

src/Pages/Home/JwtDecoder/JwtDecoder.cshtml

Lines changed: 125 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,16 @@
1616
<p>
1717
TODO more information about JWTs, link to RFC 7519, our docs...
1818
</p>
19-
<div class="alert alert-warning row">
20-
<div>
21-
<i class="glyphicon glyphicon-exclamation-sign" style="font-size: 3em" title="Warning"></i>
22-
</div>
19+
<div class="alert alert-warning d-flex align-items-center">
20+
<i class="glyphicon glyphicon-exclamation-sign" style="font-size: 3em" title="Warning"></i>
2321
<div class="mx-3">
2422
<strong>This tool does not send JWTs to any server, it only decodes the JWT locally in your browser.</strong><br/>
2523
Always be cautious when pasting JWTs, as they may contain credentials or sensitive information.
2624
</div>
2725
</div>
2826
<div class="jwt-decoder-container container">
2927
<div class="row align-items-stretch">
30-
<div class="col">
28+
<div class="col-6">
3129
<h3>Encoded JWT</h3>
3230
<div class="h-100 d-flex flex-column">
3331
<div class="form-group flex-grow-1">
@@ -44,7 +42,7 @@
4442
</div>
4543
</div>
4644
</div>
47-
<div class="col">
45+
<div class="col-6">
4846
<h3>Decoded JWT</h3>
4947
<div class="jwt-decoded-output">
5048
<h4>Header</h4>
@@ -53,6 +51,18 @@
5351
<pre id="jwt-payload" class="bg-dark text-light p-2">&nbsp;</pre>
5452
<h4>Signature</h4>
5553
<pre id="jwt-signature" class="bg-dark text-light p-2">&nbsp;</pre>
54+
<div class="jwt-signature-validation-result alert alert-success align-items-center d-none">
55+
<i class="glyphicon glyphicon-ok-sign" style="font-size: 3em" title="Valid signature"></i>
56+
<div class="result-message mx-3">This JWT has a valid signature.</div>
57+
</div>
58+
<div class="jwt-signature-validation-result alert alert-danger align-items-center d-none">
59+
<i class="glyphicon glyphicon-remove-sign" style="font-size: 3em" title="Invalid signature"></i>
60+
<div class="result-message mx-3">This JWT has an invalid signature.</div>
61+
</div>
62+
<div class="jwt-signature-validation-result alert alert-warning align-items-center d-none">
63+
<i class="glyphicon glyphicon-question-sign" style="font-size: 3em" title="Validation failed"></i>
64+
<div class="result-message mx-3">Signature validation failed.</div>
65+
</div>
5666
<div class="jwt-decoder-error d-none">
5767
<pre id="jwt-decoder-error-message" class="alert-danger p-2">&nbsp;</pre>
5868
</div>
@@ -108,31 +118,122 @@
108118
109119
async function showDecodedJwt(jwtParts, header, payload, signature) {
110120
clearError();
121+
hideSignatureValidationResults();
122+
111123
$('#jwt-header').text(header ? JSON.stringify(header, null, 2) : ' ');
112124
$('#jwt-payload').text(payload ? JSON.stringify(payload, null, 2) : ' ');
113125
$('#jwt-signature').text(signature || ' ');
114126
115127
if (jwtParts && Array.isArray(jwtParts) && jwtParts.length === 3) {
116-
await attemptSignatureValidation(jwtParts);
128+
await attemptSignatureValidation(header, jwtParts);
117129
}
118130
}
119131
120-
async function attemptSignatureValidation(jwtParts) {
132+
async function attemptSignatureValidation(header, jwtParts) {
121133
const jwksUrl = $('#jwks-url').val().trim();
122134
123135
if (jwksUrl) {
124136
await loadJwks(jwksUrl);
125137
126138
if (jwks.keys.length === 0) {
139+
showSignatureValidationResult('warning', 'No JWKs loaded. Cannot validate signature.');
127140
return;
128141
}
129142
130143
const headerAndPayload = jwtParts[0] + '.' + jwtParts[1];
131144
const signature = jwtParts[2];
132-
//const isValid = await validateSignature(headerAndPayload, signature, jwks.keys);
145+
const result = await validateSignature(header, headerAndPayload, signature, jwks.keys);
146+
147+
if (result.signatureValidated) {
148+
if (result.isValid) {
149+
showSignatureValidationResult('success');
150+
} else {
151+
showSignatureValidationResult('danger');
152+
}
153+
}
154+
else {
155+
showSignatureValidationResult('warning', result.errorMessage || 'Signature validation failed.');
156+
}
133157
}
134158
}
135159
160+
async function validateSignature(header, headerAndPayload, signature, keys) {
161+
try {
162+
if (!header || !header.alg) {
163+
return { signatureValidated: false, isValid: false, errorMessage: 'JWT header does not contain an algorithm.' };
164+
}
165+
166+
if (!header.kid) {
167+
return { signatureValidated: false, isValid: false, errorMessage: 'JWT kid is missing.' };
168+
}
169+
170+
const key = keys.find(k => k.kid && k.kid === header.kid);
171+
if (!key) {
172+
return { signatureValidated: false, isValid: false, errorMessage: `No matching key found for kid: ${header.kid}` };
173+
}
174+
175+
if (key.kty !== 'RSA' && key.kty !== 'EC') {
176+
return { signatureValidated: false, isValid: false, errorMessage: `Unsupported key type: ${key.kty}. Only RSA and EC keys are supported.` };
177+
}
178+
179+
const algorithmType = header.alg;
180+
181+
// algorithmType can be RS256, RS384, RS512, ES256, ES384, ES512, PS256, PS384, PS512, etc.
182+
if (!algorithmType || (key.kty === 'RSA' && !algorithmType.startsWith('RS')) && (key.kty === 'EC' && !algorithmType.startsWith('ES'))) {
183+
return { signatureValidated: false, isValid: false, errorMessage: `Unsupported algorithm: ${algorithmType}. Expected RS* or PS* for RSA, or ES* for EC keys.` };
184+
}
185+
186+
const algorithmName =
187+
algorithmType.startsWith('RS') ? 'RSASSA-PKCS1-v1_5' :
188+
algorithmType.startsWith('PS') ? 'RSA-PSS' :
189+
algorithmType.startsWith('ES') ? 'ECDSA' : null;
190+
191+
let algorithm = { name: algorithmName };
192+
switch (algorithmType) {
193+
case 'RS256':
194+
case 'RS384':
195+
case 'RS512':
196+
algorithm.hash = { name: 'SHA-' + algorithmType.slice(2) };
197+
break;
198+
case 'PS256':
199+
case 'PS384':
200+
case 'PS512':
201+
algorithm.hash = { name: 'SHA-' + algorithmType.slice(2) };
202+
// Salt length is either 0 or the length of the digest algorithm that was selected when this key was created.
203+
algorithm.saltLength = algorithmType === 'PS256' ? 32 : (algorithmType === 'PS384' ? 48 : 64);
204+
break;
205+
case 'ES256':
206+
algorithm.namedCurve = 'P-256';
207+
break;
208+
case 'ES384':
209+
algorithm.namedCurve = 'P-384';
210+
break;
211+
case 'ES512':
212+
algorithm.namedCurve = 'P-521';
213+
break;
214+
default:
215+
return { signatureValidated: false, isValid: false, errorMessage: `Unsupported algorithm: ${algorithmType}` };
216+
}
217+
218+
const subtle = window.crypto.subtle;
219+
const publicKey = await subtle.importKey(
220+
'jwk',
221+
key,
222+
algorithm,
223+
false,
224+
['verify']
225+
);
226+
227+
signature = signature.replace(/-/g, '+').replace(/_/g, '/'); // Replace URL-safe characters with standard Base64 characters
228+
const binarySignature = atob(signature);
229+
const signatureBuffer = Uint8Array.from(binarySignature, c => c.charCodeAt(0));
230+
const isValid = await subtle.verify(algorithm, publicKey, signatureBuffer, new TextEncoder().encode(headerAndPayload));
231+
return { signatureValidated: true, isValid: isValid };
232+
} catch (error) {
233+
return { signatureValidated: false, isValid: false, errorMessage: 'Error validating signature: ' + error.message };
234+
}
235+
}
236+
136237
async function loadJwks(jwksUrl) {
137238
if (jwks.loadedFrom === jwksUrl && jwks.keys.length > 0) {
138239
return; // Already loaded
@@ -195,5 +296,20 @@
195296
$('#jwt-decoder-error-message').text('');
196297
$('.jwt-decoder-error').addClass('d-none');
197298
}
299+
300+
function showSignatureValidationResult(type, message) {
301+
hideSignatureValidationResults();
302+
303+
const resultElement = $('.jwt-signature-validation-result.alert-' + type);
304+
resultElement.removeClass('d-none').addClass('d-flex');
305+
306+
if (message) {
307+
resultElement.find('.result-message').text(message);
308+
}
309+
}
310+
311+
function hideSignatureValidationResults() {
312+
$('.jwt-signature-validation-result').removeClass('d-flex').addClass('d-none');
313+
}
198314
</script>
199315
}

src/wwwroot/css/site.css

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,3 +107,7 @@ a.navbar-brand .icon-banner {
107107
.grants-page .card label {
108108
font-weight: bold;
109109
}
110+
111+
.jwt-decoder-container .row {
112+
height: 40vh;
113+
}

src/wwwroot/css/site.min.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/wwwroot/css/site.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,4 +118,10 @@ a.navbar-brand {
118118
font-weight: bold;
119119
}
120120
}
121+
}
122+
123+
.jwt-decoder-container {
124+
.row {
125+
height: 40vh;
126+
}
121127
}

0 commit comments

Comments
 (0)