How to Secure Your Documents with JSON Web Tokens (JWT)
In this article, we explain how to use JSON Web Token JWT to protect online documents from unauthorized access, so that you can more securely integrate online document editor development into your own web applications.
The open source office suite ONLYOFFICE Docs will be integrated here:
- Docs, Sheets, Slides, Form Template Editing Features
- High integration with Microsoft Office file formats (docx, xlsx, pptx)
- real-time collaboration
The following figures show the editing functions of documents, tables, slides and form templates, which are highly compatible with Microsoft Office. The second screenshot of the table retains the title bar of the browser window, and the other screenshots are captured in full-screen F11 mode of the webpage. .
Step 1: Create the Project Framework
Assuming that Node.js has been installed, please refer to how to install here .
Create a folder for the project, open it and run the following command:
npm init
We will be prompted to set the package name, version number, license and other information, or you can skip it directly. You can use this information to create package.json.
Then install express:
npm install express --save
The --save parameter of npm is required here, specifying that the project depends on the express package in the package.json file.
Create the following files:
- index.jx starts and configures the express server
- app/app.js query processing logic
- app/config.json variable parameters, such as port number, editor address, etc. (a json file is used here, but it is better to use a more reliable way in a real project)
index.js must contain the following code:
const express = require('express'); const cfg = require('./app/config.json'); const app = express(); app.use(express.static("public")); app.listen(cfg.port, () => { console.log(`Server is listening on ${cfg.port}`); });
The config.js file must contain the port number of the documentation editor:
{"port": 8080}
Create a public folder, add the file index.html, and add the following line to the package.json file:
"scripts": { "start": "node index.js" }
Start the running app with the following command:
npm start
Open the browser to test http://localhost:8080
Step 2: Open the document
The editor that integrates ONLYOFFICE needs to be installed ONLYOFFICE Document Server. The easiest way is to use Docker to install it with just one line of command:
docker run -i -t -d -p 9090:80 onlyoffice/documentserver
The document server must be able to send http requests to this server, and be able to receive and process requests returned by the server.
Add the editor (document server) and the address of the sample app to config.json, similar to the following:
"editors_root": "http://192.168.0.152:9090/", "example_root": "http://192.168.0.152:8080/"
At this stage, the functionality for file handling (getting files, lists, filenames, extensions, etc.) should be added to app/fileManager.js:
const fs = require('fs'); const path = require('path'); const folder = path.join(__dirname, "..", "public"); const emptyDocs = path.join(folder, "emptydocs"); function listFiles() { var files = fs.readdirSync(folder); var result = []; for (let i = 0; i < files.length; i++) { var stats = fs.lstatSync(path.join(folder, files[i])); if (!stats.isDirectory()) result.push(files[i]) } return result; } function exists(fileName) { return fs.existsSync(path.join(folder, fileName)); } function getDocType(fileName) { var ext = getFileExtension(fileName); if (".doc.docx.docm.dot.dotx.dotm.odt.fodt.ott.rtf.txt.html.htm.mht.pdf.djvu.fb2.epub.xps".indexOf(ext) != -1) return "text"; if (".xls.xlsx.xlsm.xlt.xltx.xltm.ods.fods.ots.csv".indexOf(ext) != -1) return "spreadsheet"; if (".pps.ppsx.ppsm.ppt.pptx.pptm.pot.potx.potm.odp.fodp.otp".indexOf(ext) != -1) return "presentation"; return null; } function isEditable(fileName) { var ext = getFileExtension(fileName); return ".docx.xlsx.pptx".indexOf(ext) != -1; } function createEmptyDoc(ext) { var fileName = "new." + ext; if (!fs.existsSync(path.join(emptyDocs, fileName))) return null; var destFileName = getCorrectName(fileName); fs.copyFileSync(path.join(emptyDocs, fileName), path.join(folder, destFileName)); return destFileName; } function getCorrectName(fileName) { var baseName = getFileName(fileName, true); var ext = getFileExtension(fileName); var name = baseName + "." + ext; var index = 1; while (fs.existsSync(path.join(folder, name))) { name = baseName + " (" + index + ")." + ext; index++; } return name; } function getFileName(fileName, withoutExtension) { if (!fileName) return ""; var parts = fileName.toLowerCase().split(path.sep); fileName = parts.pop(); if (withoutExtension) { fileName = fileName.substring(0, fileName.lastIndexOf(".")); } return fileName; } function getFileExtension(fileName) { if (!fileName) return null; var fileName = getFileName(fileName); var ext = fileName.toLowerCase().substring(fileName.lastIndexOf(".") + 1); return ext; } function getKey(fileName) { var stat = fs.statSync(path.join(folder, fileName)); return new Buffer(fileName + stat.mtime.getTime()).toString("base64"); } module.exports = { listFiles: listFiles, createEmptyDoc: createEmptyDoc, exists: exists, getDocType: getDocType, getFileExtension: getFileExtension, getKey: getKey, isEditable: isEditable }
Add pug package:
npm install pug --save
Now that the pug template engine is installed, it's time to delete index.html. Create a lookup folder and add the following code in index.js to connect the engine:
app.set("view engine", "pug");
Then you can create views/index.pug and add buttons to create documents and open documents:
extends master.pug block content div a(href="editors?new=docx", target="_blank") button= "Create DOCX" a(href="editors?new=xlsx", target="_blank") button= "Create XLSX" a(href="editors?new=pptx", target="_blank") button= "Create PPTX" div each val in files div a(href="editors?filename=" + val, target="_blank")= val
The logic will be explained in app/app.js: create a file (or check if it already exists), then format the editor configuration, you can read here View the details, then return to the page template:
const fm = require('./fileManager'); const cfg = require('./config.json'); function index(req, res) { res.render('index', { title: "Index", files: fm.listFiles() }); } function editors(req, res) { var fileName = ""; if (req.query.new) { var ext = req.query.new; fileName = fm.createEmptyDoc(ext); } else if (req.query.filename) { fileName = req.query.filename; } if (!fileName || !fm.exists(fileName)) { res.write("can't open/create file"); res.end(); return; } res.render('editors', { title: fileName, api: cfg.editors_root, cfg: JSON.stringify(getEditorConfig(req, fileName)) }); } function getEditorConfig(req, fileName) { var canEdit = fm.isEditable(fileName); return { width: "100%", height: "100%", type: "desktop", documentType: fm.getDocType(fileName), document: { title: fileName, url: cfg.example_root + fileName, fileType: fm.getFileExtension(fileName), key: fm.getKey(fileName), permissions: { download: true, edit: canEdit } }, editorConfig: { mode: canEdit ? "edit" : "view", lang: "en" } } } module.exports = { index: index, editors: editors };
Here, load the editor script http://docserver/web-apps/apps/api/documents/api.js then add an instance of the editor new DocsAPI.DocEditor("iframeEditor", !{cfg})
Now run the app to test it out.
Step 3: Edit the document
Edit a document, or more precisely, save your changes. This needs to handle the modification and save request sent from the document server, and specify how to respond to this request in the configuration file. For the request of the document server, please refer to here.
The document server sends a POST request with JSON content, that's why we need to connect to middleware to parse from JSON to index.js.
app.use(express.json());
In order to receive it the first time, the document server should be told what to do, adding a callbackUrl to the editor's configuration file: cfg.example_root + "callback?filename=" + fileName
Then create a callback function to get information from the document server and check the request status:
function callback(req, res) { try { var fileName = req.query.filename; !checkJwtToken(req); var status = req.body.status; switch (status) { case 2: case 3: fm.downloadSave(req.body.url, fileName); break; default: // to-do: process other statuses break; } } catch (e) { res.status(500); res.write(JSON.stringify({ error: 1, message: e.message })); res.end(); return; } res.write(JSON.stringify({ error: 0 })); res.end(); }
In this example, just focus on document save request processing, once a save file request is received, we'll get a link to our document from the POST data and save it to our file system:
functiondownloadSave(downloadFrom, saveAs) { http.get(downloadFrom, (res) => { if (res.statusCode==200) { varfile=fs.createWriteStream(path.join(folder, saveAs)); res.pipe(file); file.on('finish', function() { file.close(); }); } }); }
Now that we have a web application with document editing capabilities, let's use JWT to protect it from unauthorized access.
Step 4: Implement JWT
ONLYOFFICE uses JSON Web Tokens to secure data exchanges between editors, internal services, and storage. It requests an encrypted signature, which is then hosted in the token. This token verifies permission to perform specific operations on the data.
If you intend to use JWT it is better to use the prepared package, but here it will be completely manual to understand how it works.
Introductory theoretical foundations
JWT consists of three parts:
- header: contains meta information, for example, an encryption algorithm
- payload: data content
- hash hash: hash value based on the above two parts and the password
All three parts are JSON objects, however the JSON token itself is base64URL-encoded with all the parts joined by a dot (.).
working principle:
- Server 1 calculates a hash value based on a key and a string of header.payload.
- Token header.payload.hash generation
- Server 2 receives this token and generates a hash value based on its first two parts.
- Server 2 compares the generated token with the received token, if they match, then the data has not been modified
Now implement the JWT token for this integration instance
The editor allows the transfer of JWT tokens in the request header and body, using the request body part is better because the header space is limited, but all cases will be considered here.
If you choose the header transfer token, you need to use the payload key to add the data to the object.
If the packet body is selected to transmit the token, the payload looks like this:
{ "key": "value" }
Use the header to transmit the token:
{ "payload": { "key": "value" } }
Add key to config.json:
"jwt_secret": "supersecretkey"
To enable the JWT startup editor, you also need to set environment variables:
docker run -i -t -d -p 9090:80 -e JWT_ENABLED=true -e JWT_SECRET=supersecretkey onlyoffice/documentserver
If using the package body to transmit the token, also add a variable -e JWT_IN_BODY=true
docker run -i -t -d -p 9090:80 -e JWT_ENABLED=true -e JWT_SECRET=supersecretkey -e JWT_IN_BODY=true onlyoffice/documentserver
app/jwtManager.js contains all the logic for JWT, just add the token to the configuration when opening the editor:
if (jwt.isEnabled()) { editorConfig.token=jwt.create(editorConfig); }
The token itself has the algorithm explained above theoretically to calculate and generate, the code is as follows:
function create(payloadObj) { if (!isEnabled()) return null; var headerObj = { alg: "HS256", typ: "JWT" }; header = b64Encode(headerObj); payload = b64Encode(payloadObj); hash = calculateHash(header, payload); return header + "." + payload + "." + hash; } function calculateHash(header, payload) { return b64UrlEncode(crypto.createHmac("sha256", cfg.jwt_secret).update(header + "." + payload) .digest("base64")); }
This opens a document, but also checks the token received from the document server.
To check the package body and header, the function is simple, if there is a problem it will throw an error, otherwise, the package body and token payload will be merged after the token is confirmed:
function checkJwtToken(req) { if (!jwt.isEnabled()) return; var token = req.body.token; var inBody = true; if (!token && req.headers.authorization) { token = req.headers.authorization.substr("Bearer ".length); inBody = false; } if (!token) throw new Error("Expected JWT token"); var payload = jwt.verify(token); if (!payload) throw new Error("JWT token validation failed"); if (inBody) { Object.assign(req.body, payload); } else { Object.assign(req.body, payload.payload); } }
The validation function is also very simple:
function verify(token) { if (!isEnabled()) return null; if (!token) return null; var parts = token.split("."); if (parts.length != 3) { return null; } var hash = calculateHash(parts[0], parts[1]); if (hash !== parts[2]) return null; return b64Decode(parts[1]); }
Take a look at the methods in jwtManager:
The create method gets an object with data, for example:
{ "key": "value" }
Create the JWT header:
{ "alg": "HS256", "typ": "JWT" }
This method then uses these two objects to create a JSON string, encoded as base64url. Then connect the two lines with dots to generate a hash based on your key, in this example we use the supersecretkey.
As a result we get the following token:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSJ9.ozm44FMRAlWXB0PhJg935wyOkp7wtj1jXvgEGIS0iig
The verification method obtains this token, disassembles it using the dot as a separator, gets the first two parts and the hash value after it, and then compares the hash value generated by itself with the received hash value. If they match, the payload is encoded and returns a JSON object.
You can also dig deeper into the token, learn how it was created, and look for Open source libraries for different programming languages.
Note that this is just a minimal implementation of JWT, the standard is very informative and takes into account various complexities, such as the limited lifetime of tokens. So we recommend using ready-made JWT related packages in real practice.
We hope this example will help you integrate ONLYOFFICE in your web application, using JWT to protect online collaborative editing functions, more integration examples can be found at github Check out the research, also available at ONLYOFFICE API documentation Find more technical details about the JWT implementation on .
R5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSJ9.ozm44FMRAlWXB0PhJg935wyOkp7wtj1jXvgEGIS0iig`
The verification method obtains this token, disassembles it using the dot as a separator, gets the first two parts and the hash value after it, and then compares the hash value generated by itself with the received hash value. If they match, the payload is encoded and returns a JSON object.
You can also dig deeper into the token, learn how it was created, and look for Open source libraries for different programming languages.
Note that this is just a minimal implementation of JWT, the standard is very informative and takes into account various complexities, such as the limited lifetime of tokens. So we recommend using ready-made JWT related packages in real practice.
We hope this example will help you integrate ONLYOFFICE in your web application, using JWT to protect online collaborative editing functions, more integration examples can be found at github Check out the research, also available at ONLYOFFICE API documentation Find more technical details about the JWT implementation on .