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 in config.cors for 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 false from 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.