How to Authenticate an Embedded Editor with JWT
Build a secure JWT token endpoint for embedded editors, with CKEditor and TinyMCE claim differences, runnable code, refresh handling, and troubleshooting.

To authenticate an embedded editor with JWT, keep your signing key on your application backend, authenticate the current application user, issue the claims required by the specific editor service, and let the editor request that token from a protected endpoint. The browser may hold and send the token, but it must never mint tokens or receive your signing secret.
A JWT is a signed, readable claims container. It is not an encrypted password vault. Put an editor user identifier and service-specific authorization claims in it; never put API keys, passwords, private keys, or other secrets in the payload.
Authentication flow
- The user signs in to your application.
- Your backend checks the session and whether that user may use the editor service.
- The editor calls an authenticated application endpoint such as
/api/editor-token. - Your backend creates the vendor’s required claims and signs them with the algorithm and key configured for that deployment.
- The editor sends the JWT to the vendor service, then refreshes it according to that integration’s rules.
The endpoint is an authorization boundary. Do not make it a public token minting URL. CKEditor documents that a token endpoint should return a token only after the user proves their identity; TinyMCE likewise expects a backend-issued token provider. See the CKEditor authentication documentation and TinyMCE AI JWT authentication guide.

Claims and algorithms are vendor-specific
There is no universal editor JWT profile. Copying claim names or signing algorithms from one vendor into another can produce rejected tokens.
| Integration | Required or documented details | Operational consequence |
|---|---|---|
| CKEditor Cloud Services | aud, iat, and sub; optional exp; HS256, HS384, and HS512; tokens no older than 24 hours |
Use your CKEditor environment ID as aud, the application user ID as sub, and keep the secret server-side. |
| CKEditor Converters API | JWT is sent as a bearer token in the Authorization header. |
Generate the token on your backend so the converter access key is not exposed. |
| TinyMCE AI hosted cloud | aud, sub, iat, and exp; public/private key setup; RS-family or PS-family algorithms, with RS256 recommended. |
Configure the matching public key with TinyMCE and return the token through the token provider. |
| TinyMCE AI on-premises | The on-premises guide specifies HS256. | Confirm the deployment type before choosing an algorithm; do not reuse the hosted-cloud setup blindly. |
aud identifies the target environment, iat records issuance time, sub identifies the user, and exp limits validity. Permission or role claims must match the services the user is allowed to call. Use seconds since the Unix epoch unless the vendor’s library explicitly says otherwise.
Node.js example: Express token endpoint
This example shows the server-side shape. Replace the claim names, audience, and signing configuration with the profile for your editor deployment. The requireUser middleware represents your existing session or access-token verification.
import express from "express";
import jwt from "jsonwebtoken";
const app = express();
const port = process.env.PORT || 3000;
function requireUser(req, res, next) {
// Replace this with your real session or access-token check.
const user = req.user;
if (!user) return res.status(401).json({ error: "authentication_required" });
next();
}
app.get("/api/editor-token", requireUser, (req, res) => {
const now = Math.floor(Date.now() / 1000);
const user = req.user;
// Example CKEditor-style claims. Verify the exact profile first.
const claims = {
aud: process.env.EDITOR_ENVIRONMENT_ID,
iat: now,
sub: String(user.id),
exp: now + 15 * 60,
// Add only the documented roles or permissions this user needs.
};
const token = jwt.sign(claims, process.env.EDITOR_JWT_SECRET, {
algorithm: "HS256"
});
res.json({ token });
});
app.listen(port, () => console.log(`Token service listening on ${port}`));
Store EDITOR_JWT_SECRET in a secret manager or protected environment variable. Never commit it, return it to the browser, or log the complete token in production.
Python example: Flask token endpoint
import os
import time
import jwt
from flask import Flask, jsonify, g, request
app = Flask(__name__)
def require_user():
# Replace this with your session or bearer-token verification.
user_id = request.headers.get("X-Demo-User")
if not user_id:
return None
return {"id": user_id}
@app.get("/api/editor-token")
def editor_token():
user = require_user()
if not user:
return jsonify(error="authentication_required"), 401
now = int(time.time())
claims = {
"aud": os.environ["EDITOR_ENVIRONMENT_ID"],
"iat": now,
"sub": str(user["id"]),
"exp": now + 15 * 60,
}
token = jwt.encode(
claims,
os.environ["EDITOR_JWT_SECRET"],
algorithm="HS256",
)
return jsonify(token=token)
if __name__ == "__main__":
app.run(port=3000)
Client configuration and token refresh
Configure the editor’s documented token-provider callback to call your endpoint with the user’s authenticated cookies or access token. Return the response shape the integration expects. TinyMCE AI’s provider can return a token property or a raw token as documented. The first request happens during initialization and periodic refreshes typically occur about once an hour. TinyMCE states that the editor is not ready until its first token is obtained, so surface that failure in your application instead of rendering an apparently usable but unauthorized editor.
tinymce.init({
selector: "#editor",
plugins: "ai",
tinymceai_token_provider: async (callback) => {
try {
const response = await fetch("/api/editor-token", {
credentials: "include",
headers: { Accept: "application/json" }
});
if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
const data = await response.json();
callback(data.token);
} catch (error) {
console.error("Editor authentication failed", error);
callback(null);
}
}
});
For a bearer-token application, send Authorization: Bearer <session-token> to your endpoint. For cookie sessions, enable credentials and configure same-site and cross-origin cookie policy correctly. Do not treat toolbar visibility, disabled buttons, or other browser-side checks as authorization; client code can be bypassed.
Testing checklist
- Authenticated user receives a token; anonymous user receives 401.
- A user without the editor permission is denied before signing.
- Claims contain the exact audience, subject, issuance time, and required expiration.
- The chosen algorithm matches the editor deployment and configured key.
- An expired token is rejected and a refresh obtains a new one.
- System clocks are synchronized on the application host and the vendor environment.
- The real editor request succeeds, not only a decoded-token inspection.
- Logs record status and correlation IDs without recording secrets or full JWTs.
Common errors and fixes
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 from your token endpoint | Missing session, cookie, or bearer token | Check browser credentials, CORS, cookie SameSite settings, and session middleware. |
| Invalid audience | aud does not match the configured environment |
Copy the environment identifier from the vendor configuration and compare exact casing. |
| Token expired or not yet valid | Incorrect timestamp units or clock drift | Use Unix seconds, synchronize clocks, and issue a short but practical lifetime. |
| Signature verification failed | Wrong secret, public key, or algorithm | Confirm hosted versus on-premises deployment and configure the matching algorithm and key. |
| Editor never becomes ready | Initial provider request failed or returned the wrong JSON shape | Inspect the network response, return the documented token property, and handle non-2xx responses. |
| Works locally, fails in production | Missing environment secret, proxy rewriting, or unsynchronized server time | Check deployment secrets, HTTPS forwarding headers, and host time. |
| Converter request is unauthorized | JWT was not sent as a bearer token | Set Authorization: Bearer YOUR_JWT on the converter request. |
| Feature denied after valid signature | Permission or role claim is absent or too broad | Use the vendor’s documented permission names and issue only the user’s allowed roles. |

Security, reliability, and performance
Protect keys and reduce authority
Use a secret manager, restrict read access, rotate keys according to your operational policy, and separate development credentials from production credentials. A stolen symmetric secret allows token forgery. With asymmetric signing, protect the private key and publish only the matching public key through the vendor’s configuration. Keep tokens short-lived and scoped to the services the user actually needs.
Make the endpoint dependable
The endpoint should be a small, fast operation with no editor rendering work. Return explicit 401 or 403 responses, set a bounded timeout on downstream calls, and include a request ID for diagnosis. Cache no token longer than its remaining lifetime, and ensure concurrent refreshes do not stampede your identity provider. A failed refresh should disable the affected feature and offer a retry rather than silently continuing with an expired token.
Plan for browser and network edge cases
Cross-origin editors need an exact allowed origin, HTTPS, and credentials configuration. A reverse proxy must preserve the authorization header and forward the correct scheme. If users can open multiple tabs, each tab may refresh independently; that is safe but can increase traffic. Do not put JWTs in query strings, where they can leak through history and referrer logs.
Hosted cloud versus on-premises deployment
Choose the deployment before writing the signer. Hosted TinyMCE AI uses an asymmetric public/private-key arrangement and documents RS-family or PS-family options, while its on-premises AI guide specifies HS256. CKEditor Cloud Services documents HMAC algorithms and a maximum accepted token age of 24 hours. Compare the claim set, permission model, key storage, refresh behavior, and failure response for the exact service you selected.
Or skip the browser setup
If your next task is generating screenshots of the authenticated editor or its documentation, ScreenshotNeo can handle the capture request directly. Read the ScreenshotNeo API documentation for the full option set.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
Cookie banners, newsletter popups, and chat widgets are removed before the shot. Bot checks, blank pages, failed loads, timeouts, and cache hits are never billed, and response headers identify the page verdict and billing status. An MCP server lets Claude, Cursor, and other MCP clients take screenshots with take_screenshot, inspect pages with get_page_info, and create PDFs with capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Create a free ScreenshotNeo account.
FAQ
Can I sign a JWT in browser JavaScript?
No. The browser cannot safely hold the signing secret or private key. It should request a token from your authenticated backend.
Should JWT payloads contain document text?
No. Payloads are readable by whoever receives the token. Keep them small and limited to identity and authorization claims.
How long should an editor token last?
Use the shortest lifetime that supports the integration’s refresh behavior. Follow the vendor’s limit; CKEditor documents acceptance of tokens no older than 24 hours, while TinyMCE hosted AI requires an expiration claim.
Why does a valid JWT still get rejected?
Validity covers signature and time, not configuration. Check audience, subject format, permissions, deployment type, algorithm, and the exact public or shared key.
Is a hidden toolbar button a security control?
No. Enforce authorization in the token endpoint and every server-controlled operation. Client-side restrictions are only user-interface behavior.