Skip to content

Commit 2ebabbd

Browse files
authored
Jwt debugger (#22)
1 parent 76f7e77 commit 2ebabbd

File tree

8 files changed

+375
-4
lines changed

8 files changed

+375
-4
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
- [x] JSON Formatter
2424
- [x] SQL Formatter
2525
- [x] Regex Tester
26-
- [ ] JWT Debugger
26+
- [x] JWT Debugger
2727
- [ ] Number Base Converter
2828
- [ ] URL Encode/Decode
2929
- [ ] HTML Entity Encode/Decode

package.json

+5
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,8 @@
170170
"@types/enzyme-adapter-react-16": "^1.0.6",
171171
"@types/history": "4.7.6",
172172
"@types/jest": "^26.0.15",
173+
"@types/jsonwebtoken": "^8.5.4",
174+
"@types/lodash.isequal": "^4.5.5",
173175
"@types/marked": "^2.0.4",
174176
"@types/node": "14.14.10",
175177
"@types/pngjs": "^6.0.1",
@@ -253,14 +255,17 @@
253255
"@fortawesome/react-fontawesome": "^0.1.14",
254256
"@tailwindcss/typography": "^0.4.1",
255257
"caniuse-lite": "^1.0.30001246",
258+
"classnames": "^2.3.1",
256259
"dayjs": "^1.10.6",
257260
"diff": "^5.0.0",
258261
"electron-debug": "^3.1.0",
259262
"electron-log": "^4.2.4",
260263
"electron-store": "^8.0.0",
261264
"electron-updater": "^4.3.4",
262265
"history": "^5.0.0",
266+
"jsonwebtoken": "^8.5.1",
263267
"jsqr": "^1.4.0",
268+
"lodash.isequal": "^4.5.0",
264269
"marked": "^2.1.3",
265270
"pngjs": "^6.0.0",
266271
"qrcode": "^1.4.4",

src/components/Main.tsx

+7
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import SqlFormatter from './sql/SqlFormatter';
1414
import JsonFormatter from './json/JsonFormatter';
1515
import QRCodeReader from './qrcode/QrCodeReader';
1616
import RegexTester from './regex/RegexTester';
17+
import JwtDebugger from './jwt/JwtDebugger';
1718
import Auto from './auto/Auto';
1819

1920
const defaultRoutes = [
@@ -83,6 +84,12 @@ const defaultRoutes = [
8384
name: 'SQL Formatter',
8485
Component: SqlFormatter,
8586
},
87+
{
88+
icon: <FontAwesomeIcon icon="key" />,
89+
path: '/jwt-debugger',
90+
name: 'JWT Debugger',
91+
Component: JwtDebugger,
92+
},
8693
];
8794

8895
const Main = () => {

src/components/auto/Auto.tsx

+10
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { clipboard, ipcRenderer } from 'electron';
22
import path from 'path';
33
import React, { useEffect, useState } from 'react';
4+
import { decode } from 'jsonwebtoken';
45
import { useHistory, useLocation } from 'react-router-dom';
56

67
const detectRouteData = (value: string) => {
@@ -20,6 +21,15 @@ const detectRouteData = (value: string) => {
2021
// ignore
2122
}
2223

24+
try {
25+
const validJwt = decode(value);
26+
if (validJwt) {
27+
return { route: '/jwt-debugger', state: { input1: value } };
28+
}
29+
} catch (e) {
30+
// ignore
31+
}
32+
2333
return {};
2434
};
2535

src/components/jwt/JwtDebugger.tsx

+246
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
import React, { useEffect, useState } from 'react';
2+
import classNames from 'classnames';
3+
import _isEqual from 'lodash.isequal';
4+
import { ipcRenderer, clipboard } from 'electron';
5+
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
6+
import {
7+
decode,
8+
verify,
9+
sign,
10+
Algorithm,
11+
JwtPayload,
12+
JwtHeader,
13+
Secret,
14+
} from 'jsonwebtoken';
15+
import { useLocation } from 'react-router-dom';
16+
17+
interface LocationState {
18+
input1: string;
19+
}
20+
const jwtInputPlaceHolder =
21+
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU';
22+
23+
const JwtDebugger = () => {
24+
const location = useLocation<LocationState>();
25+
const [jwtInput, setJwtInput] = useState(jwtInputPlaceHolder);
26+
const [header, setHeader] = useState<JwtHeader>({
27+
alg: 'HS256',
28+
typ: 'JWT',
29+
});
30+
const [payload, setPayload] = useState<JwtPayload>({
31+
sub: '1234567890',
32+
name: 'John Doe',
33+
iat: 1516239022,
34+
});
35+
const [algorithm, setAlgorithm] = useState<Algorithm>('HS256');
36+
const [secret, setSecret] = useState<Secret>('123456');
37+
38+
const [verifyError, setVerifyError] = useState(false);
39+
40+
// for opening files
41+
const [opening, setOpening] = useState(false);
42+
// for copying payload
43+
const [copied, setCopied] = useState(false);
44+
45+
const formatForDisplay = (json: JwtHeader | JwtPayload) =>
46+
JSON.stringify(json, null, 4);
47+
48+
const decodeJWT = (token: string) => decode(token, { complete: true });
49+
50+
const handleJwtInputChanged = (evt: { target: { value: string } }) => {
51+
setJwtInput(evt.target.value);
52+
};
53+
54+
useEffect(() => {
55+
let jwt;
56+
try {
57+
jwt = sign(payload, secret, { algorithm, header });
58+
setJwtInput(jwt);
59+
setVerifyError(false);
60+
} catch (e) {
61+
setVerifyError(true);
62+
}
63+
}, [payload, secret, algorithm, header]);
64+
65+
useEffect(() => {
66+
try {
67+
const jwt = decodeJWT(jwtInput);
68+
if (jwt) {
69+
if (!_isEqual(jwt.header, header)) setHeader(jwt.header);
70+
if (!_isEqual(jwt.payload, payload)) setPayload(jwt.payload);
71+
}
72+
} catch (e) {
73+
// eslint-disable-next-line no-alert
74+
alert(e.message);
75+
}
76+
try {
77+
verify(jwtInput, secret, { algorithms: [algorithm] });
78+
setVerifyError(false);
79+
} catch (e) {
80+
setVerifyError(true);
81+
}
82+
// eslint-disable-next-line react-hooks/exhaustive-deps
83+
}, [jwtInput]);
84+
85+
useEffect(() => {
86+
if (location.state && location.state.input1) {
87+
setJwtInput(location.state.input1);
88+
}
89+
}, [location]);
90+
91+
const handleChangePayload = (evt: { target: { value: string } }) => {
92+
try {
93+
setPayload(JSON.parse(evt.target.value));
94+
} catch (e) {
95+
// eslint-disable-next-line no-alert
96+
alert(e.message);
97+
}
98+
};
99+
100+
const handleChangeHeader = (evt: { target: { value: string } }) => {
101+
try {
102+
const h = JSON.parse(evt.target.value);
103+
setHeader(h);
104+
if (h.alg !== algorithm) {
105+
const alg = h.alg as Algorithm;
106+
setAlgorithm(alg);
107+
}
108+
} catch (e) {
109+
// eslint-disable-next-line no-alert
110+
alert(e.message);
111+
}
112+
};
113+
114+
const handleChangeAlgorithm = (evt: { target: { value: string } }) => {
115+
const alg = evt.target.value as Algorithm;
116+
setAlgorithm(alg);
117+
if (alg !== header.alg) {
118+
setHeader({
119+
...header,
120+
alg,
121+
});
122+
}
123+
};
124+
125+
const handleChangeSecret = (evt: { target: { value: string } }) => {
126+
setSecret(evt.target.value);
127+
};
128+
129+
const handleOpenInput = async () => {
130+
setOpening(true);
131+
const content = await ipcRenderer.invoke('open-file', []);
132+
setJwtInput(Buffer.from(content).toString());
133+
setOpening(false);
134+
};
135+
136+
const handleClipboardInput = () => {
137+
setJwtInput(clipboard.readText());
138+
};
139+
140+
const handleCopyOutput = () => {
141+
setCopied(true);
142+
clipboard.write({ text: JSON.stringify(payload) });
143+
setTimeout(() => setCopied(false), 500);
144+
};
145+
146+
return (
147+
<div className="flex flex-col min-h-full">
148+
<div className="flex justify-between mb-1">
149+
<span className="flex space-x-2">
150+
<button type="button" className="btn" onClick={handleClipboardInput}>
151+
Clipboard
152+
</button>
153+
<button
154+
type="button"
155+
className="btn"
156+
onClick={handleOpenInput}
157+
disabled={opening}
158+
>
159+
Open...
160+
</button>
161+
</span>
162+
<span
163+
className={classNames({
164+
'ml-auto space-x-1': true,
165+
'text-green-500': !verifyError,
166+
'text-red-500': verifyError,
167+
})}
168+
>
169+
<FontAwesomeIcon icon="check-circle" />
170+
<span>
171+
{verifyError ? 'Invalid Signature' : 'Signature verified'}
172+
</span>
173+
</span>
174+
</div>
175+
<div className="flex flex-1 min-h-full space-x-4">
176+
<section className="flex flex-col flex-1">
177+
<textarea
178+
className="flex-1 min-h-full p-2 bg-white rounded-md"
179+
onChange={handleJwtInputChanged}
180+
value={jwtInput}
181+
disabled={opening}
182+
/>
183+
</section>
184+
<section className="flex flex-col flex-1">
185+
<div className="flex-1 p-2 bg-gray-100 rounded-md">
186+
<div className="mb-4">
187+
<p className="mb-1">Header:</p>
188+
<textarea
189+
className="flex-1 w-full h-40 p-2 rounded-md"
190+
onChange={handleChangeHeader}
191+
value={formatForDisplay(header)}
192+
disabled={opening}
193+
/>
194+
</div>
195+
<div className="mb-4">
196+
<section className="flex items-center justify-between mb-1">
197+
<p>Payload:</p>
198+
<span className="flex space-x-4">
199+
<button
200+
type="button"
201+
className="btn"
202+
onClick={handleCopyOutput}
203+
disabled={copied}
204+
>
205+
{copied ? 'Copied' : 'Copy'}
206+
</button>
207+
</span>
208+
</section>
209+
<textarea
210+
className="flex-1 w-full h-40 p-2 rounded-md"
211+
onChange={handleChangePayload}
212+
value={formatForDisplay(payload)}
213+
disabled={opening}
214+
/>
215+
</div>
216+
<div className="mb-4">
217+
<p className="mb-1">Secret:</p>
218+
<input
219+
className="flex-1 w-full px-2 py-1 bg-white rounded-md"
220+
onChange={handleChangeSecret}
221+
placeholder="Secret"
222+
value={secret.toString()}
223+
/>
224+
</div>
225+
<div className="mb-4">
226+
<p className="mb-1">Algorithm:</p>
227+
<select
228+
className="p-2 rounded-md cursor-pointer"
229+
name="algorithm"
230+
id="algorithm"
231+
onChange={handleChangeAlgorithm}
232+
value={algorithm}
233+
>
234+
<option value="HS256">HS256</option>
235+
<option value="HS384">HS384</option>
236+
<option value="HS512">HS512</option>
237+
</select>
238+
</div>
239+
</div>
240+
</section>
241+
</div>
242+
</div>
243+
);
244+
};
245+
246+
export default JwtDebugger;

src/helpers/fontAwesome.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import {
1616
faQrcode,
1717
faSearch,
1818
faTimesCircle,
19+
faKey,
20+
faCheckCircle,
1921
faRobot,
2022
} from '@fortawesome/free-solid-svg-icons';
2123

@@ -33,6 +35,8 @@ library.add(
3335
faRegistered,
3436
faSearch,
3537
faTimesCircle,
36-
faRobot,
37-
faCamera
38+
faCamera,
39+
faKey,
40+
faCheckCircle,
41+
faRobot
3842
);

src/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "plainbelt",
33
"productName": "PlainBelt",
4-
"version": "0.0.11",
4+
"version": "0.0.12",
55
"description": "A plain toolbelt for developers",
66
"main": "./main.prod.js",
77
"author": {

0 commit comments

Comments
 (0)