Login, JWTs & Security
Jspreadsheet Server is identity-agnostic: it does not create users, store passwords or issue tokens. Your application authenticates users with any identity provider (Amazon Cognito, Auth0, Keycloak, your own JWT issuer), and the server receives the resulting token on every request. Your authorization hooks decide what that token may do. This page covers the full path: token transport, verification and the trust boundaries.
How tokens reach the server
Your application obtains a token from your identity provider at login. From then on, the token accompanies every interaction with the server.
WebSocket transport: the auth handshake
Everything you put in the auth option of the client extension is sent in the connection handshake and arrives in the server hooks as the auth argument.
REST transport: the Bearer header
The token is sent as a Bearer header: Authorization: Bearer <token>. If the user is acting through an invitation, append the invitation code after a comma: Bearer <token>,<invitation>; the server splits it and exposes both as auth.token and auth.invitation.
Connecting the client extension with a token
import jspreadsheet from 'jspreadsheet';
import client from '@jspreadsheet/client';
jspreadsheet.setExtensions({ client });
const remote = client.connect({
url: 'https://sheets.example.com',
auth: { token },
});
Where authorization happens
The three gate hooks
Every request funnels through the same three hooks, for sockets and REST alike:
| Hook | Fires | Typical decision |
|---|---|---|
beforeConnect(auth) |
On connection / every API request | Is this a client we talk to at all? |
beforeLoad(guid, auth) |
Before a document is loaded or read | May this user see this document? |
beforeChange(guid, changes, auth) |
Before each operation is applied | May this user modify it, and this specific method? |
The hooks receive the raw auth object and return true/false. The recommended structure, resolving the user's level for the document (owner 2 / editor 1 / viewer 0) and deriving every decision from it, is covered with full code in Authentication.
Owner-only operations
Ownership-sensitive operations need an additional check inside beforeChange: methods like setConfig, setUsers, deleteUsers and snapshot management should require level 2 even when the user may otherwise edit:
const requireOwnership = (method) => [
'setConfig', 'setUsers', 'deleteUsers',
'createSnapshot', 'restoreSnapshot', 'deleteSnapshot',
].includes(method);
beforeChange: async function(guid, changes, auth) {
let level = await getUserLevel(guid, auth);
if (requireOwnership(changes.method)) {
return level === 2;
}
return level >= 1;
}
Verify tokens in your hooks
Signature verification is your hooks' responsibility
The server passes the auth object to your hooks exactly as it was received. It performs no token validation of its own. Do not trust the claims of a token whose signature has not been checked against your identity provider's key: an unverified payload can be forged with any user identity. Reading the payload without verifying is acceptable only when an upstream gateway has already verified the token before it reaches the server.
The subject claim is your user identity
Once verified, the token's subject claim is your user identity. Match it against the document's user_id for ownership decisions, as shown in Authentication.
Cache your provider's signing keys
Verification runs on every hook call, so keep it cheap: cache your provider's signing keys instead of fetching them per request.
Trust boundaries to keep in mind
A guid identifies a document; it does not grant access
Document guids are unguessable UUIDs, but treat them as public knowledge. They appear in URLs, logs and shared links. Every access decision must come from the hooks, never from possession of the guid.
Public documents are public for writing too
This is the case if your getUserLevel returns editor for them (for example, privacy unset → level 1). If you want public read-only documents, return 0 instead and let beforeChange reject writes.
Invitations are bearer secrets
The invitation hash grants the level stored with it to anyone who presents it. Generate them with a CSPRNG, deliver them privately, and delete them (deleteUsers) to revoke access.
Form endpoints are self-authorized
Formify endpoints (/api/<guid>/<worksheet>/formify) intentionally bypass your hooks so anonymous users can submit forms. The bypass is limited in scope: the routes only work when the worksheet declares a formify configuration, and submissions can only insert one row restricted to the columns declared on the form. Enabling formify on a worksheet publishes it, so audit which worksheets carry that option.
The agent stores are user-scoped by you
Chat sessions and token usage callbacks receive the auth object; scope your queries by the verified user identity so users cannot read each other's conversations. See Extensions.
Production checklist
- TLS everywhere: tokens travel in query strings (socket handshake) and headers; terminate HTTPS/WSS at your proxy. See Nginx.
- Verify token signatures in your hooks against your identity provider's keys; reject expired tokens.
- Lock down CORS: the default examples use
origin: "*"for development; set your real origins inconfig.corsfor production. - Keep secrets in the environment: license keys, JWT secrets, S3 and database credentials belong in
.env/secret managers, never in code. - Scope database credentials: the adapter only needs its own database; the agent's S3 user only needs its bucket.
- Rate-limit at the proxy: the server applies operations serially per document, but auth-failure storms and anonymous formify posts are best throttled in Nginx.
- Log denials, not tokens: log a
falsefrom a hook together with the guid and the user identity; never log the raw token. - Rotate keys: follow your identity provider's key rotation; with a shared secret plan a dual-secret rollover.
Related pages
- Authentication: access levels, owner checks, invitation flow, full hook examples.
- Sharing & privacy: the invited-users model.
- Extensions: how routes registered by extensions inherit these gates.
- Scaling: the guid-routing model these tokens flow through.