diff --git a/package.json b/package.json index 847c3fb..4237c39 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "classnames": "^2.5.1", "framer-motion": "^10.8.3", "lodash": "^4.17.21", + "lucide-react": "^0.544.0", "prettier": "^3.3.3", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/src/components/DevAreaTools/JwtDecoder.jsx b/src/components/DevAreaTools/JwtDecoder.jsx new file mode 100644 index 0000000..07e1966 --- /dev/null +++ b/src/components/DevAreaTools/JwtDecoder.jsx @@ -0,0 +1,173 @@ +import React, { useState } from "react"; + +export function base64UrlToBase64(base64url) { + if (typeof base64url !== "string") throw new Error("Invalid input for base64UrlToBase64"); + let base64 = base64url.replace(/-/g, "+").replace(/_/g, "/"); + const pad = base64.length % 4; + if (pad === 2) base64 += "=="; + else if (pad === 3) base64 += "="; + else if (pad === 1) throw new Error("Invalid base64url string"); + return base64; +} + +export function decodeBase64UrlJson(input) { + const b64 = base64UrlToBase64(input); + try { + const decoded = atob(b64); + try { + const percentDecoded = decodeURIComponent( + decoded + .split("") + .map((c) => { + const code = c.charCodeAt(0).toString(16).padStart(2, "0"); + return `%${code}`; + }) + .join("") + ); + return JSON.parse(percentDecoded); + } catch (e) { + return JSON.parse(decoded); + } + } catch (err) { + throw new Error("Failed to decode base64url JSON: " + (err && err.message)); + } +} + +export function parseJWT(token) { + if (typeof token !== "string") throw new Error("Token must be a string"); + const parts = token.trim().split("."); + return { + parts, + header: parts[0] ? decodeBase64UrlJson(parts[0]) : null, + payload: parts[1] ? decodeBase64UrlJson(parts[1]) : null, + signature: parts[2] || null, + }; +} + +export default function JWTDecoder() { + const [token, setToken] = useState(""); + const [header, setHeader] = useState(null); + const [payload, setPayload] = useState(null); + const [signature, setSignature] = useState(null); + const [error, setError] = useState(null); + + const handleDecode = () => { + setError(null); + setHeader(null); + setPayload(null); + setSignature(null); + if (!token.trim()) { + setError("Please paste a JWT token."); + return; + } + try { + const parsed = parseJWT(token); + if (!parsed.parts || parsed.parts.length < 2) { + setError("Token does not have the expected parts (header.payload[.signature])."); + return; + } + setHeader(parsed.header); + setPayload(parsed.payload); + setSignature(parsed.signature); + } catch (e) { + setError(e.message || String(e)); + } + }; + + const handleClear = () => { + setToken(""); + setHeader(null); + setPayload(null); + setSignature(null); + setError(null); + }; + + const copyPayload = async () => { + if (!payload) return; + try { + await navigator.clipboard.writeText(JSON.stringify(payload, null, 2)); + } catch {} + }; + + return ( +
+ Paste your JWT below and decode it safely on the client-side. +
+ ++ ⚠️ This tool only decodes client-side. Never paste production tokens. +
+