Skip to content

Security model

This page describes what the code does. It isn’t a compliance statement; certifications and legal assessments are up to the operator.

  • A sign-in creates a session row and a short-lived token (HS256 JWT signed with SESSION_SECRET) that carries only the session ID and user ID. It expires after SESSION_HOURS (12 by default).
  • The session row is checked on every request, so signing out, resetting a password, disabling a user and suspending an organization take effect immediately.
  • Tokens travel in the Authorization: Bearer header, not in cookies, which avoids third-party-cookie problems inside the Teams iframe and cross-site request forgery. In the browser the token is kept in local storage; inside Teams only in memory.
  • Inside Teams, Teams SSO provides an Entra access token for this API; in the browser, MSAL does (popup, organizations authority, so work and school accounts only).
  • The API verifies each token’s signature against Microsoft’s published keys, its audience (your Application ID URI or client ID), its issuer (Microsoft, for the token’s own tenant, v1.0 or v2.0), and that it carries the access_as_user scope.
  • A Microsoft token signs someone in only if their tenant ID belongs to a registered organization, and then only as an existing user of that organization, or as a new user when automatic joining is on and a license is free.
  • For Microsoft 365 features, the token’s tenant must match the organization’s connected tenant; otherwise That Microsoft account belongs to a different organization.

The API never has its own access to customer data. For each OneDrive, Teams, mail or directory request, the browser sends the user’s Entra token (X-Entra-Token header), and the API exchanges it for a Microsoft Graph token with the user’s delegated permissions (OAuth 2.0 on-behalf-of). It can only do what the user can do. Graph tokens are kept in the API’s memory only, not in the database.

Data Stored?
Files opened from or saved to OneDrive and Teams No. The contents pass through the API and aren’t kept. The file name, ID and size are recorded in the activity log.
Files opened from the user’s computer No. They stay in the browser. The file name is recorded in the activity log.
Documents sent for signature Yes, in PostgreSQL: the original PDF, the signed PDF, field values (including signature images), and recipients’ names, emails, IP addresses and browsers.
Signatures adopted in the editor Only in that browser’s local storage.
Passwords Only as scrypt hashes.
Email confirmation and reset tokens Only their SHA-256 hashes.
Signing links Only a SHA-256 hash of each link’s token. The token is an HMAC derived from SESSION_SECRET, so links can be re-sent without being stored.

Nothing is deleted automatically; see Database.

  • In the organization-facing API, every query is scoped to the organization of the signed-in user’s session, not to an organization ID taken from the request.
  • Signing requests, users and activity are visible only within the organization (and to super admins).
  • A Microsoft 365 organization can be connected to only one account.
  • Signers reach exactly one recipient’s view through their link, and only while the sender’s organization is active.

The API sends a Content Security Policy (via Helmet) that allows scripts and connections only from the app itself, plus:

  • login.microsoftonline.com (Microsoft sign-in),
  • cdn.jsdelivr.net and tessdata.projectnaptha.com (the OCR engine and language data),
  • Google Fonts (typed signatures),
  • checkout.stripe.com as a form target.

frame-ancestors allows the app itself and Microsoft Teams and Microsoft 365 hosts (teams.microsoft.com, *.teams.microsoft.com, *.teams.microsoft.us, *.cloud.microsoft, *.office.com, *.office365.com, *.microsoft365.com, *.skype.com); no other site can frame it. X-Frame-Options isn’t sent, because it can’t allow Teams. object-src is none.

What Limit (per client IP)
Sign-up, sign-in, Microsoft sign-in, email confirmation, password reset 20 requests per minute
Public signing pages (/api/sign/…) 60 requests per minute
JSON request bodies 2 MB
PDF uploads (sending for signature, saving to Microsoft 365) MAX_UPLOAD_MB (25 MB)
Signature images PNG, under 600 KB

Over the limit, the API answers 429 with Too many requests. Please wait a minute and try again. Other endpoints aren’t rate-limited by the API; add limits at your reverse proxy if you need them.

  • Passwords are hashed with scrypt (N=16384, r=8, p=1, 64-byte key) and a random 16-byte salt per password, after Unicode NFKC normalization, and compared in constant time.
  • Policy: 10 to 200 characters; a single repeated character, and passwords starting with 0123456789, 1234567890 or password, are rejected. There are no composition rules.
  • Forgot password answers the same way whether or not the account exists. Reset links expire after an hour and sign the user out everywhere.
  • Email/password sign-in exists only for Tenant Owners who signed up and for super admins. There’s no built-in multi-factor authentication for these accounts; everyone else signs in with Microsoft, where your organization’s Entra policies (including MFA) apply.

The SHA-256 of the original and of the signed PDF are recorded, every signing step is in the write-only activity log, and the signed PDF carries a certificate of completion. The signed PDF is not signed with an X.509 digital certificate. See Certificate of completion.