Skip to content

Commit 7c3026a

Browse files
authored
Merge pull request #69 from rcbj/feature/issue-40
Add JWT JWS Validation option to the JWT Deserialization Page. #40
2 parents 6eab79d + a938023 commit 7c3026a

File tree

2 files changed

+195
-15
lines changed

2 files changed

+195
-15
lines changed

client/public/token_detail.html

+39
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,45 @@
6767
</textarea>
6868
</td>
6969
</tr>
70+
<tr>
71+
<td>
72+
<div class="tooltip"><label>Verification Type: </label><span class="tooltiptext">The JWT Verification Type.</span>
73+
</div>
74+
</td>
75+
<td>
76+
<select class="stored" id="jwt_verification_type" name="jwt_verification_type">
77+
<option value="hmac" selected="selected">HMAC Secret</option>
78+
<option value="x509">X.509 Certificate (PEM)</option>
79+
<option value="jwks">JWKS (JSON)</option>
80+
<option value="jwks_url">JWKS (URL)</option>
81+
</select>
82+
</td>
83+
</tr>
84+
<tr>
85+
<td>
86+
<div class="tooltip"><label>JWT Verification Key/URL:</label><span class="tooltiptext">The JWT Verification Key/URL.</span>
87+
</div>
88+
</td>
89+
<td>
90+
<textarea rows=6 cols=50 id="jwt_verification_key" name="jwt_verification_key"></textarea>
91+
</td>
92+
</tr>
93+
<tr>
94+
<td>
95+
<input class="btn2" type="submit" value="Verify Signature" onclick="return token_detail.verifyJWT();" />
96+
</td>
97+
<td>&nbsp;
98+
</td>
99+
</tr>
100+
<tr>
101+
<td>
102+
<div class="tooltip"><label>Verification Output:</label><span class="tooltiptext">Verification Output.</span>
103+
</div>
104+
</td>
105+
<td>
106+
<textarea rows=6 cols=50 id="jwt_verification_output" name="jwt_verification_output"></textarea>
107+
</td>
108+
</tr>
70109
</tbody>
71110
</table>
72111
</fieldset>

client/src/token_detail.js

+156-15
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,163 @@ var log = bunyan.createLogger({ name: 'token_detail',
1313
log.info("Log initialized. logLevel=" + log.level());
1414
const jwt = require('jsonwebtoken');
1515

16+
function getParameterByName(name, url)
17+
{
18+
log.debug("Entering getParameterByName().");
19+
if (!url)
20+
{
21+
url = window.location.search;
22+
}
23+
var urlParams = new URLSearchParams(url);
24+
return urlParams.get(name);
25+
}
26+
1627
function decodeJWT(jwt_) {
1728
return jwt.decode(jwt_, {complete: true});
1829
}
1930

31+
async function verifyJWT() {
32+
var type = getParameterByName('type');
33+
var jwt_verification_type = document.getElementById("jwt_verification_type").value;
34+
var jwt_verification_key = document.getElementById("jwt_verification_key").value;
35+
var jwt_ = "";
36+
if (type == 'access') {
37+
jwt_ = localStorage.getItem("token_access_token");
38+
} else if (type == 'refresh') {
39+
jwt_ = localStorage.getItem("token_refresh_token");
40+
} else if (type == 'id') {
41+
jwt_ = localStorage.getItem("token_id_token");
42+
} else if (type == 'refresh_access') {
43+
jwt_ = localStorage.getItem("refresh_access_token");
44+
} else if (type == 'refresh_refresh') {
45+
jwt_ = localStorage("refresh_refresh_token");
46+
} else if (type == 'refresh_id') {
47+
jwt_ = localStorage.getItem('refresh_id_token');
48+
} else {
49+
log.error('Unknown token type encountered.');
50+
}
51+
52+
try {
53+
const [headerB64, payloadB64, signatureB64] = jwt_.split('.');
54+
if (!headerB64 || !payloadB64 || !signatureB64) throw new Error('Invalid JWT format.');
55+
56+
const header = JSON.parse(atobUrl(headerB64));
57+
var isValid = false;
58+
if (jwt_verification_type === 'hmac') {
59+
isValid = await verifyHMAC(jwt_, jwt_verification_key, header.alg);
60+
} else if (jwt_verification_type === 'x509') {
61+
isValid = await verifyX509(jwt_, jwt_verification_key, header.alg);
62+
} else if (jwt_verification_type === 'jwks') {
63+
isValid = await verifyJWKS(jwt_, JSON.parse(jwt_verification_key));
64+
} else if (jwt_verification_type === 'jwks_url') {
65+
const response = await fetch(jwt_verification_key);
66+
if (!response.ok) throw new Error('Failed to fetch JWKS.');
67+
isValid = await verifyJWKS(jwt_, await response.json());
68+
} else {
69+
throw new Error('Unsupported verification method.');
70+
}
71+
} catch (err) {
72+
log.error("Error while verifying JWT: " + err.message);
73+
}
74+
75+
document.getElementById('jwt_verification_output').value = "Signature Verified: " + isValid;
76+
}
77+
78+
function atobUrl(input) {
79+
input = input.replace(/-/g, '+').replace(/_/g, '/');
80+
const pad = '==='.slice(0, (4 - input.length % 4) % 4);
81+
return atob(input + pad);
82+
}
83+
84+
function base64UrlToUint8Array(base64UrlString) {
85+
const binary = atobUrl(base64UrlString);
86+
const bytes = new Uint8Array(binary.length);
87+
88+
for (let i = 0; i < binary.length; i++) {
89+
bytes[i] = binary.charCodeAt(i);
90+
}
91+
92+
return bytes;
93+
}
94+
95+
function pemToArrayBuffer(pem) {
96+
const binary = atob(pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, ''));
97+
const buffer = new Uint8Array(binary.length);
98+
99+
for (let i = 0; i < binary.length; i++) {
100+
buffer[i] = binary.charCodeAt(i);
101+
}
102+
103+
return buffer.buffer;
104+
}
105+
106+
async function verifyHMAC(jwt_, secret, alg = 'HS256') {
107+
const encoder = new TextEncoder();
108+
const algo = { HS256: 'SHA-256', HS384: 'SHA-384', HS512: 'SHA-512' }[alg];
109+
if (!algo) throw new Error('Unsupported HMAC algorithm: ' + alg);
110+
111+
const key = await crypto.subtle.importKey(
112+
'raw',
113+
encoder.encode(secret),
114+
{ name: 'HMAC', hash: { name: algo } },
115+
false,
116+
['verify']
117+
);
118+
const data = encoder.encode(jwt_.split('.').slice(0, 2).join('.'));
119+
const signature = base64UrlToUint8Array(jwt_.split('.')[2]);
120+
121+
return await crypto.subtle.verify('HMAC', key, signature, data);
122+
}
123+
124+
async function verifyX509(jwt_, pem, alg = 'RS256') {
125+
const encoder = new TextEncoder();
126+
const algo = { RS256: 'SHA-256', RS384: 'SHA-384', RS512: 'SHA-512' }[alg];
127+
if (!algo) throw new Error('Unsupported RSA algorithm: ' + alg);
128+
129+
const key = await crypto.subtle.importKey(
130+
'spki',
131+
pemToArrayBuffer(pem),
132+
{ name: 'RSASSA-PKCS1-v1_5', hash: { name: algo } },
133+
false,
134+
['verify']
135+
);
136+
const data = encoder.encode(jwt_.split('.').slice(0, 2).join('.'));
137+
const signature = base64UrlToUint8Array(jwt_.split('.')[2]);
138+
139+
return await crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
140+
}
141+
142+
async function verifyJWKS(jwt_, jwks) {
143+
const header = JSON.parse(atobUrl(jwt_.split('.')[0]));
144+
if (!header.kid) throw new Error('No "kid" found in JWT header.');
145+
146+
const jwk = jwks.keys.find(k => k.kid === header.kid);
147+
if (!jwk) throw new Error('Matching "kid" not found in JWKS.');
148+
if (jwk.kty !== 'RSA') throw new Error('Only RSA keys are supported.');
149+
150+
const encoder = new TextEncoder();
151+
const algo = { RS256: 'SHA-256', RS384: 'SHA-384', RS512: 'SHA-512' }[header.alg];
152+
if (!algo) throw new Error('Unsupported algorithm: ' + header.alg);
153+
154+
const key = await crypto.subtle.importKey(
155+
'jwk',
156+
{ kty: jwk.kty, n: jwk.n, e: jwk.e },
157+
{ name: 'RSASSA-PKCS1-v1_5', hash: { name: algo } },
158+
false,
159+
['verify']
160+
);
161+
const data = encoder.encode(jwt_.split('.').slice(0, 2).join('.'));
162+
const signature = base64UrlToUint8Array(jwt_.split('.')[2]);
163+
164+
return await crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
165+
}
166+
167+
$(document).on("change", "#jwt_verification_type", function() {
168+
if (this.value == "jwks_url") {
169+
document.getElementById('jwt_verification_key').value = localStorage.getItem("jwks_endpoint");
170+
}
171+
});
172+
20173
window.onload = function() {
21174
log.debug("Entering onload function.");
22175
const type = getParameterByName('type');
@@ -43,19 +196,7 @@ window.onload = function() {
43196
document.getElementById('jwt_payload').value = JSON.stringify(decodedJWT.payload, null, 2);
44197
}
45198

46-
function getParameterByName(name, url)
47-
{
48-
log.debug("Entering getParameterByName().");
49-
if (!url)
50-
{
51-
url = window.location.search;
52-
}
53-
var urlParams = new URLSearchParams(url);
54-
return urlParams.get(name);
55-
}
56-
57199
module.exports = {
58-
decodeJWT
59-
};
60-
61-
200+
decodeJWT,
201+
verifyJWT
202+
};

0 commit comments

Comments
 (0)