Skip to content

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.

On this page

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 configurationValid tokenNo tokenBad or expired token
O3O_EMBED_JWT_SECRET setaccepted401 token_required401 invalid_token / 401 token_expired
No secret, O3O_EMBED_ALLOW_UNSIGNED=1 (DEV)token ignored, plain config usedacceptedtoken ignored, plain config used
No secret, O3O_EMBED_ALLOW_UNSIGNED=0401 embed_auth_not_configured401 embed_auth_not_configured401 embed_auth_not_configured

Payload#

Payload claims

  • documentobjectrequired
    Same as config.document.
  • editorobjectrequired
    Same as config.editor.
  • uiobjectoptional
    Same as config.ui.
  • expnumber, Unix secondsrequired
    Expiry, at most 24 hours after iat (or after now without iat). 60 seconds of clock skew allowed.
  • iatnumber, Unix secondsoptional
    Issued-at time. Recommended.
JSONPayload before signing
{
  "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#

Sign the config
// 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.

JavaScriptPage receiving the token
// 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
    }
  }
});