Security model
This page describes what the code does. It isn’t a compliance statement; certifications and legal assessments are up to the operator.
Sessions
Section titled “Sessions”- 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 afterSESSION_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: Bearerheader, 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.
Microsoft sign-in and token validation
Section titled “Microsoft sign-in and token validation”- Inside Teams, Teams SSO provides an Entra access token for this API; in the browser, MSAL does (popup,
organizationsauthority, 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_userscope. - 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.
Microsoft Graph: on behalf of the user
Section titled “Microsoft Graph: on behalf of the user”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.
What’s stored, and what isn’t
Section titled “What’s stored, and what isn’t”| 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.
Tenant isolation
Section titled “Tenant isolation”- 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.
Browser protections
Section titled “Browser protections”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.netandtessdata.projectnaptha.com(the OCR engine and language data),- Google Fonts (typed signatures),
checkout.stripe.comas 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.
Rate limits and request sizes
Section titled “Rate limits and request sizes”| 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
Section titled “Passwords”- 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,1234567890orpassword, 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.
Signing integrity
Section titled “Signing integrity”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.