excalidraw/excalidraw · error · ExcalidrawError
Failed to verify JWT
Error message
Failed to verify JWT
What it means
ExcalidrawError('Failed to verify JWT') thrown when verifyJWT() rejects during the ExcalidrawPlus iframe export handshake. Any failure inside verifyJWT — missing public key env var, malformed token, signature mismatch, expired token, or invalid JSON payload — is caught, logged with the underlying message, and rewrapped as this generic error so the specific cryptographic reason is not leaked to the requester.
Source
Thrown at excalidraw-app/ExcalidrawPlusIframeExport.tsx:175
const handleMessage = async (event: MessageEvent<MESSAGE_FROM_PLUS>) => {
if (event.origin !== EXCALIDRAW_PLUS_ORIGIN) {
throw new ExcalidrawError("Invalid origin");
}
if (event.data.type === EVENT_REQUEST_SCENE) {
if (!event.data.jwt) {
throw new ExcalidrawError("JWT is missing");
}
try {
try {
await verifyJWT({
token: event.data.jwt,
publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY,
});
} catch (error: any) {
console.error(`Failed to verify JWT: ${error.message}`);
throw new ExcalidrawError("Failed to verify JWT");
}
const parsedSceneData: MESSAGE_SCENE_DATA = await parseSceneData({
rawAppStateString: localStorage.getItem(
STORAGE_KEYS.LOCAL_STORAGE_APP_STATE,
),
rawElementsString: localStorage.getItem(
STORAGE_KEYS.LOCAL_STORAGE_ELEMENTS,
),
});
event.source!.postMessage(parsedSceneData, {
targetOrigin: EXCALIDRAW_PLUS_ORIGIN,
});
} catch (error) {
const responseData: MESSAGE_ERROR = {
type: "ERROR",
message:View on GitHub (pinned to abeeaeba21)
Solutions
- Check the console for 'Failed to verify JWT: <message>' which carries the underlying reason (expired, invalid signature, etc.).
- Ensure VITE_APP_PLUS_EXPORT_PUBLIC_KEY is set at build time and matches the current Plus backend signing key.
- Request a fresh JWT from the Plus parent and retry.
- Verify the client clock is accurate (exp check is time-sensitive).
Example fix
// before
await verifyJWT({ token: jwt, publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY });
// throws -> generic 'Failed to verify JWT'
// after: surface the real reason locally for debugging
if (!import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY) {
console.error("VITE_APP_PLUS_EXPORT_PUBLIC_KEY is not set");
}
try {
await verifyJWT({ token: jwt, publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY });
} catch (e) {
console.error("JWT verification failed:", e.message);
} Defensive patterns
Strategy: validation
Validate before calling
if (!import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY) {
// cannot verify — do not attempt; key must be set at build time
}
// verify expiry client-side to give a clearer error pre-flight
const payload = JSON.parse(atob(event.data.jwt.split(".")[1]));
if (payload.exp && payload.exp < Math.floor(Date.now()/1000)) {
// expired — request a new token before calling verifyJWT
} Type guard
const looksLikeJwt = (t: string): boolean =>
typeof t === "string" && t.split(".").length === 3; Try / catch
try {
await verifyJWT({ token: jwt, publicKey: import.meta.env.VITE_APP_PLUS_EXPORT_PUBLIC_KEY });
} catch (e) {
// console has 'Failed to verify JWT: <reason>' — surface generic error to requester
} Prevention
- Set VITE_APP_PLUS_EXPORT_PUBLIC_KEY at build time and rotate with the backend.
- Request fresh JWTs for each Plus-export session.
- Keep client clocks accurate; exp is enforced.
- Never expose verification details to the requesting parent.
When it happens
Trigger: event.data.jwt is expired, signed with the wrong key, malformed (missing header/payload/signature), or VITE_APP_PLUS_EXPORT_PUBLIC_KEY is unset/invalid; or crypto.subtle.importKey/verify rejects. The error is thrown before any scene data is exposed.
Common situations: Clock skew causing a valid token to appear expired, the Plus backend rotating its signing key without updating VITE_APP_PLUS_EXPORT_PUBLIC_KEY, a tampered token, an environment where the public key env var was not set at build time, or an old token reused after expiry.
Related errors
- Elements or appstate is missing.
- Scene is empty, nothing to export.
- Invalid or disallowed library URL: "${libraryUrl}"
AI-assisted analysis of excalidraw/excalidraw@abeeaeba21 (2026-08-12).
Data as JSON: /api/errors/b93da648c92033b0.
Report an issue: GitHub.