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), the server receives the resulting token on every request, and your authorization hooks decide what that token may do.

How tokens reach the server

The token your application obtains at login 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 in Authentication.

Owner-only operations

Ownership-sensitive methods (setConfig, setUsers, deleteUsers, snapshot management) should require level 2 in beforeChange, even for users who 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 auth to your hooks exactly as received and performs no token validation. Check the signature against your identity provider's key before trusting any claim: an unverified payload can be forged with any user identity. Skipping verification is acceptable only when an upstream gateway has already verified the token.

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: 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: they appear in URLs, logs and shared links. Access decisions must come from the hooks, never from possession of the guid.

Public documents are public for writing too

That happens if your getUserLevel returns editor for them (for example, privacy unset → level 1). For 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 routes only work when the worksheet declares a formify configuration, and a submission can only insert one row, restricted to the columns declared on the form. Enabling formify publishes the worksheet, so audit which worksheets carry it.

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.