diff --git a/README.md b/README.md index 195ab6f..3ebe853 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ - [x] JSON Formatter - [x] SQL Formatter - [x] Regex Tester -- [ ] JWT Debugger +- [x] JWT Debugger - [ ] Number Base Converter - [ ] URL Encode/Decode - [ ] HTML Entity Encode/Decode diff --git a/package.json b/package.json index 50d95b0..b37c128 100644 --- a/package.json +++ b/package.json @@ -170,6 +170,8 @@ "@types/enzyme-adapter-react-16": "^1.0.6", "@types/history": "4.7.6", "@types/jest": "^26.0.15", + "@types/jsonwebtoken": "^8.5.4", + "@types/lodash.isequal": "^4.5.5", "@types/marked": "^2.0.4", "@types/node": "14.14.10", "@types/pngjs": "^6.0.1", @@ -253,6 +255,7 @@ "@fortawesome/react-fontawesome": "^0.1.14", "@tailwindcss/typography": "^0.4.1", "caniuse-lite": "^1.0.30001246", + "classnames": "^2.3.1", "dayjs": "^1.10.6", "diff": "^5.0.0", "electron-debug": "^3.1.0", @@ -260,7 +263,9 @@ "electron-store": "^8.0.0", "electron-updater": "^4.3.4", "history": "^5.0.0", + "jsonwebtoken": "^8.5.1", "jsqr": "^1.4.0", + "lodash.isequal": "^4.5.0", "marked": "^2.1.3", "pngjs": "^6.0.0", "qrcode": "^1.4.4", diff --git a/src/components/Main.tsx b/src/components/Main.tsx index 82b75a4..ed393aa 100644 --- a/src/components/Main.tsx +++ b/src/components/Main.tsx @@ -14,6 +14,7 @@ import SqlFormatter from './sql/SqlFormatter'; import JsonFormatter from './json/JsonFormatter'; import QRCodeReader from './qrcode/QrCodeReader'; import RegexTester from './regex/RegexTester'; +import JwtDebugger from './jwt/JwtDebugger'; import Auto from './auto/Auto'; const defaultRoutes = [ @@ -83,6 +84,12 @@ const defaultRoutes = [ name: 'SQL Formatter', Component: SqlFormatter, }, + { + icon: , + path: '/jwt-debugger', + name: 'JWT Debugger', + Component: JwtDebugger, + }, ]; const Main = () => { diff --git a/src/components/auto/Auto.tsx b/src/components/auto/Auto.tsx index c92bc40..16676a9 100644 --- a/src/components/auto/Auto.tsx +++ b/src/components/auto/Auto.tsx @@ -1,6 +1,7 @@ import { clipboard, ipcRenderer } from 'electron'; import path from 'path'; import React, { useEffect, useState } from 'react'; +import { decode } from 'jsonwebtoken'; import { useHistory, useLocation } from 'react-router-dom'; const detectRouteData = (value: string) => { @@ -20,6 +21,15 @@ const detectRouteData = (value: string) => { // ignore } + try { + const validJwt = decode(value); + if (validJwt) { + return { route: '/jwt-debugger', state: { input1: value } }; + } + } catch (e) { + // ignore + } + return {}; }; diff --git a/src/components/jwt/JwtDebugger.tsx b/src/components/jwt/JwtDebugger.tsx new file mode 100644 index 0000000..2a6b829 --- /dev/null +++ b/src/components/jwt/JwtDebugger.tsx @@ -0,0 +1,246 @@ +import React, { useEffect, useState } from 'react'; +import classNames from 'classnames'; +import _isEqual from 'lodash.isequal'; +import { ipcRenderer, clipboard } from 'electron'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { + decode, + verify, + sign, + Algorithm, + JwtPayload, + JwtHeader, + Secret, +} from 'jsonwebtoken'; +import { useLocation } from 'react-router-dom'; + +interface LocationState { + input1: string; +} +const jwtInputPlaceHolder = + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.keH6T3x1z7mmhKL1T3r9sQdAxxdzB6siemGMr_6ZOwU'; + +const JwtDebugger = () => { + const location = useLocation(); + const [jwtInput, setJwtInput] = useState(jwtInputPlaceHolder); + const [header, setHeader] = useState({ + alg: 'HS256', + typ: 'JWT', + }); + const [payload, setPayload] = useState({ + sub: '1234567890', + name: 'John Doe', + iat: 1516239022, + }); + const [algorithm, setAlgorithm] = useState('HS256'); + const [secret, setSecret] = useState('123456'); + + const [verifyError, setVerifyError] = useState(false); + + // for opening files + const [opening, setOpening] = useState(false); + // for copying payload + const [copied, setCopied] = useState(false); + + const formatForDisplay = (json: JwtHeader | JwtPayload) => + JSON.stringify(json, null, 4); + + const decodeJWT = (token: string) => decode(token, { complete: true }); + + const handleJwtInputChanged = (evt: { target: { value: string } }) => { + setJwtInput(evt.target.value); + }; + + useEffect(() => { + let jwt; + try { + jwt = sign(payload, secret, { algorithm, header }); + setJwtInput(jwt); + setVerifyError(false); + } catch (e) { + setVerifyError(true); + } + }, [payload, secret, algorithm, header]); + + useEffect(() => { + try { + const jwt = decodeJWT(jwtInput); + if (jwt) { + if (!_isEqual(jwt.header, header)) setHeader(jwt.header); + if (!_isEqual(jwt.payload, payload)) setPayload(jwt.payload); + } + } catch (e) { + // eslint-disable-next-line no-alert + alert(e.message); + } + try { + verify(jwtInput, secret, { algorithms: [algorithm] }); + setVerifyError(false); + } catch (e) { + setVerifyError(true); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [jwtInput]); + + useEffect(() => { + if (location.state && location.state.input1) { + setJwtInput(location.state.input1); + } + }, [location]); + + const handleChangePayload = (evt: { target: { value: string } }) => { + try { + setPayload(JSON.parse(evt.target.value)); + } catch (e) { + // eslint-disable-next-line no-alert + alert(e.message); + } + }; + + const handleChangeHeader = (evt: { target: { value: string } }) => { + try { + const h = JSON.parse(evt.target.value); + setHeader(h); + if (h.alg !== algorithm) { + const alg = h.alg as Algorithm; + setAlgorithm(alg); + } + } catch (e) { + // eslint-disable-next-line no-alert + alert(e.message); + } + }; + + const handleChangeAlgorithm = (evt: { target: { value: string } }) => { + const alg = evt.target.value as Algorithm; + setAlgorithm(alg); + if (alg !== header.alg) { + setHeader({ + ...header, + alg, + }); + } + }; + + const handleChangeSecret = (evt: { target: { value: string } }) => { + setSecret(evt.target.value); + }; + + const handleOpenInput = async () => { + setOpening(true); + const content = await ipcRenderer.invoke('open-file', []); + setJwtInput(Buffer.from(content).toString()); + setOpening(false); + }; + + const handleClipboardInput = () => { + setJwtInput(clipboard.readText()); + }; + + const handleCopyOutput = () => { + setCopied(true); + clipboard.write({ text: JSON.stringify(payload) }); + setTimeout(() => setCopied(false), 500); + }; + + return ( +
+
+ + + + + + + + {verifyError ? 'Invalid Signature' : 'Signature verified'} + + +
+
+
+