Editor embedding
Signing the config with JWT
Sign the config with HS256 JWT and O3O_EMBED_JWT_SECRET on Node.js, Python, PHP and C# servers, and how the page uses the token.
In production every config must be signed by your server, so users cannot change document.url, editor.mode or editor.user in the browser. O3O uses JWT HS256 with the shared secret O3O_EMBED_JWT_SECRET (at least 32 characters).
| Server configuration | Valid token | No token | Bad or expired token |
|---|---|---|---|
O3O_EMBED_JWT_SECRET set | accepted | 401 token_required | 401 invalid_token / 401 token_expired |
No secret, O3O_EMBED_ALLOW_UNSIGNED=1 (DEV) | token ignored, plain config used | accepted | token ignored, plain config used |
No secret, O3O_EMBED_ALLOW_UNSIGNED=0 | 401 embed_auth_not_configured | 401 embed_auth_not_configured | 401 embed_auth_not_configured |
Payload#
Payload claims
documentobjectrequiredSame asconfig.document.editorobjectrequiredSame asconfig.editor.uiobjectoptionalSame asconfig.ui.expnumber, Unix secondsrequiredExpiry, at most 24 hours afteriat(or after now withoutiat). 60 seconds of clock skew allowed.iatnumber, Unix secondsoptionalIssued-at time. Recommended.
{
"document": {
"url": "https://files.example.com/contracts/42.docx",
"title": "Hợp đồng số 42.docx",
"fileType": "docx",
"key": "contract-42-v7"
},
"editor": {
"mode": "edit",
"lang": "vi",
"user": {
"id": "u-1001",
"name": "Nguyễn Văn A"
},
"callbackUrl": "https://app.example.com/o3o/callback"
},
"ui": {
"closeButton": true
},
"iat": 1790000000,
"exp": 1790003600
}Sign on the server#
// npm install jsonwebtoken
const jwt = require("jsonwebtoken");
const config = {
document: { url: "https://files.example.com/contracts/42.docx", title: "Contract 42.docx", fileType: "docx", key: "contract-42-v7" },
editor: { mode: "edit", lang: "vi", user: { id: "u-1001", name: "Jane Doe" }, callbackUrl: "https://app.example.com/o3o/callback" },
ui: { closeButton: true }
};
// jsonwebtoken adds iat itself; expiresIn sets exp (24 hours at most)
const token = jwt.sign(config, process.env.O3O_EMBED_JWT_SECRET, { algorithm: "HS256", expiresIn: "1h" });
console.log(token);# pip install pyjwt
import os
import time
import jwt
config = {
"document": {"url": "https://files.example.com/contracts/42.docx", "title": "Contract 42.docx", "fileType": "docx", "key": "contract-42-v7"},
"editor": {"mode": "edit", "lang": "vi", "user": {"id": "u-1001", "name": "Jane Doe"}, "callbackUrl": "https://app.example.com/o3o/callback"},
"ui": {"closeButton": True},
}
now = int(time.time())
token = jwt.encode({**config, "iat": now, "exp": now + 3600}, os.environ["O3O_EMBED_JWT_SECRET"], algorithm="HS256")
print(token)<?php
// composer require firebase/php-jwt
require __DIR__ . '/vendor/autoload.php';
use Firebase\JWT\JWT;
$config = [
'document' => ['url' => 'https://files.example.com/contracts/42.docx', 'title' => 'Contract 42.docx', 'fileType' => 'docx', 'key' => 'contract-42-v7'],
'editor' => ['mode' => 'edit', 'lang' => 'vi', 'user' => ['id' => 'u-1001', 'name' => 'Jane Doe'], 'callbackUrl' => 'https://app.example.com/o3o/callback'],
'ui' => ['closeButton' => true],
];
$now = time();
$token = JWT::encode($config + ['iat' => $now, 'exp' => $now + 3600], getenv('O3O_EMBED_JWT_SECRET'), 'HS256');
echo $token, PHP_EOL;// dotnet add package Microsoft.IdentityModel.JsonWebTokens
using System.Text;
using System.Text.Json.Nodes;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;
var now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
var payload = new JsonObject
{
["document"] = new JsonObject { ["url"] = "https://files.example.com/contracts/42.docx", ["title"] = "Contract 42.docx", ["fileType"] = "docx", ["key"] = "contract-42-v7" },
["editor"] = new JsonObject
{
["mode"] = "edit", ["lang"] = "vi",
["user"] = new JsonObject { ["id"] = "u-1001", ["name"] = "Jane Doe" },
["callbackUrl"] = "https://app.example.com/o3o/callback"
},
["ui"] = new JsonObject { ["closeButton"] = true },
["iat"] = now,
["exp"] = now + 3600
};
// HS256 needs a key of at least 32 bytes, matching the 32-character minimum of O3O_EMBED_JWT_SECRET
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Environment.GetEnvironmentVariable("O3O_EMBED_JWT_SECRET")!));
var token = new JsonWebTokenHandler().CreateToken(payload.ToJsonString(), new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
Console.WriteLine(token);Use the token in the browser#
With a valid token, the gate uses document, editor, ui from the token and ignores same-named fields sent alongside. So the page only needs token and events.
// Your server sends the token to the page (never the secret). Sending only the token is enough.
const editor = new O3O.Editor("o3o-editor", {
token: "<token signed by your server>",
events: {
onError: (e) => {
if (e.code === "token_expired") location.reload(); // get a fresh token
}
}
});