Skip to content

Commit df67825

Browse files
WIP
1 parent 540465a commit df67825

File tree

2 files changed

+220
-15
lines changed

2 files changed

+220
-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 (Static 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

+181-15
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,188 @@ 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 verifyRSJWT(jwt_, jwt_verification_key, header.alg);
62+
} else if (jwt_verification_type === 'jwks') {
63+
isValid = await verifyWithJWKS(jwt_, JSON.parse(jwt_verification_key));
64+
} else if (jwt_verification_type === 'jwks_url') {
65+
isValid = await verifyWithJWKS(jwt_, await fetchJWKS(jwt_verification_key));
66+
} else {
67+
throw new Error('Unsupported verification method.');
68+
}
69+
} catch (err) {
70+
log.error("Error while verifying JWT: " + err.message);
71+
}
72+
73+
document.getElementById('jwt_verification_output').value = "Signature Verified: " + isValid;
74+
}
75+
76+
function atobUrl(input) {
77+
input = input.replace(/-/g, '+').replace(/_/g, '/');
78+
const pad = input.length % 4 ? '='.repeat(4 - (input.length % 4)) : '';
79+
return atob(input + pad);
80+
}
81+
82+
function base64UrlToUint8Array(base64UrlString) {
83+
const binary = atobUrl(base64UrlString);
84+
const len = binary.length;
85+
const bytes = new Uint8Array(len);
86+
for (let i = 0; i < len; i++) {
87+
bytes[i] = binary.charCodeAt(i);
88+
}
89+
return bytes;
90+
}
91+
92+
async function verifyHMAC(jwt, secret, alg = 'HS256') {
93+
const encoder = new TextEncoder();
94+
const algoMap = { HS256: 'SHA-256', HS384: 'SHA-384', HS512: 'SHA-512' };
95+
const algo = algoMap[alg];
96+
if (!algo) throw new Error('Unsupported HMAC algorithm: ' + alg);
97+
98+
const key = await crypto.subtle.importKey(
99+
'raw',
100+
encoder.encode(secret),
101+
{ name: 'HMAC', hash: { name: algo } },
102+
false,
103+
['verify']
104+
);
105+
106+
const data = encoder.encode(jwt.split('.').slice(0, 2).join('.'));
107+
const signature = base64UrlToUint8Array(jwt.split('.')[2]);
108+
109+
return await crypto.subtle.verify('HMAC', key, signature, data);
110+
}
111+
112+
async function verifyRSJWT(jwt, pem, alg = 'RS256') {
113+
const algoMap = {
114+
RS256: 'SHA-256',
115+
RS384: 'SHA-384',
116+
RS512: 'SHA-512'
117+
};
118+
const algo = algoMap[alg];
119+
if (!algo) throw new Error('Unsupported RSA algorithm: ' + alg);
120+
121+
const keyData = pemToArrayBuffer(pem);
122+
const key = await crypto.subtle.importKey(
123+
'spki',
124+
keyData,
125+
{ name: 'RSASSA-PKCS1-v1_5', hash: { name: algo } },
126+
false,
127+
['verify']
128+
);
129+
130+
const encoder = new TextEncoder();
131+
const data = encoder.encode(jwt.split('.').slice(0, 2).join('.'));
132+
const signature = base64UrlToUint8Array(jwt.split('.')[2]);
133+
134+
return await crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
135+
}
136+
137+
function pemToArrayBuffer(pem) {
138+
const b64Lines = pem.replace(/-----[^-]+-----/g, '').replace(/\s+/g, '');
139+
const binary = atob(b64Lines);
140+
const len = binary.length;
141+
const buffer = new ArrayBuffer(len);
142+
const view = new Uint8Array(buffer);
143+
for (let i = 0; i < len; i++) {
144+
view[i] = binary.charCodeAt(i);
145+
}
146+
return buffer;
147+
}
148+
149+
async function verifyWithJWKS(jwt, jwks) {
150+
const header = JSON.parse(atobUrl(jwt.split('.')[0]));
151+
const kid = header.kid;
152+
if (!kid) throw new Error('No "kid" found in JWT header.');
153+
154+
const jwk = jwks.keys.find(k => k.kid === kid);
155+
if (!jwk) throw new Error('Matching "kid" not found in JWKS.');
156+
157+
const algoMap = {
158+
RS256: 'SHA-256',
159+
RS384: 'SHA-384',
160+
RS512: 'SHA-512'
161+
};
162+
163+
const algo = algoMap[header.alg];
164+
if (!algo) throw new Error('Unsupported algorithm: ' + header.alg);
165+
166+
const key = await importJWK(jwk, algo);
167+
const encoder = new TextEncoder();
168+
const data = encoder.encode(jwt.split('.').slice(0, 2).join('.'));
169+
const signature = base64UrlToUint8Array(jwt.split('.')[2]);
170+
171+
return await crypto.subtle.verify('RSASSA-PKCS1-v1_5', key, signature, data);
172+
}
173+
174+
async function fetchJWKS(url) {
175+
const response = await fetch(url);
176+
if (!response.ok) throw new Error('Failed to fetch JWKS.');
177+
return await response.json();
178+
}
179+
180+
async function importJWK(jwk, hashAlgo) {
181+
if (jwk.kty !== 'RSA') throw new Error('Only RSA keys are supported in this example.');
182+
183+
const publicKey = {
184+
kty: jwk.kty,
185+
n: jwk.n,
186+
e: jwk.e
187+
};
188+
189+
return await crypto.subtle.importKey(
190+
'jwk',
191+
publicKey,
192+
{ name: 'RSASSA-PKCS1-v1_5', hash: { name: hashAlgo } },
193+
false,
194+
['verify']
195+
);
196+
}
197+
20198
window.onload = function() {
21199
log.debug("Entering onload function.");
22200
const type = getParameterByName('type');
@@ -43,19 +221,7 @@ window.onload = function() {
43221
document.getElementById('jwt_payload').value = JSON.stringify(decodedJWT.payload, null, 2);
44222
}
45223

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-
57224
module.exports = {
58-
decodeJWT
59-
};
60-
61-
225+
decodeJWT,
226+
verifyJWT
227+
};

0 commit comments

Comments
 (0)