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

  1. Check the console for 'Failed to verify JWT: <message>' which carries the underlying reason (expired, invalid signature, etc.).
  2. Ensure VITE_APP_PLUS_EXPORT_PUBLIC_KEY is set at build time and matches the current Plus backend signing key.
  3. Request a fresh JWT from the Plus parent and retry.
  4. 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

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


AI-assisted analysis of excalidraw/excalidraw@abeeaeba21 (2026-08-12). Data as JSON: /api/errors/b93da648c92033b0. Report an issue: GitHub.