Essential security practices for Documenso integrations: API key management, webhook verification, document access control, and self-hosted signing certificate configuration.
documenso-install-auth setup// NEVER hardcode keys
const BAD = new Documenso({ apiKey: "api_abc123..." }); // Exposed in source
// ALWAYS use environment variables
const GOOD = new Documenso({ apiKey: process.env.DOCUMENSO_API_KEY! });
Key management rules:
.env (never committed) or a secrets manager (Vault, AWS Secrets Manager)# .gitignore — always include
.env
.env.*
!.env.example
// Support dual keys during rotation
function getApiKey(): string {
// Try primary first, fall back to secondary during rotation
return process.env.DOCUMENSO_API_KEY_PRIMARY
?? process.env.DOCUMENSO_API_KEY_SECONDARY
?? (() => { throw new Error("No Documenso API key configured"); })();
}
// Rotation procedure:
// 1. Generate new key in Documenso dashboard
// 2. Set as DOCUMENSO_API_KEY_SECONDARY, deploy
// 3. Verify secondary key works
// 4. Move secondary to PRIMARY, deploy
// 5. Revoke old key in dashboard
import { timingSafeEqual } from "crypto";
function verifyWebhookSecret(req: Request): boolean {
const received = req.headers["x-documenso-secret"] as string;
const expected = process.env.DOCUMENSO_WEBHOOK_SECRET!;
if (!received || !expected) return false;
// Use constant-time comparison to prevent timing attacks
return timingSafeEqual(
Buffer.from(received, "utf8"),
Buffer.from(expected, "utf8")
);
}
# Python equivalent
import hmac, os
from flask import request
def verify_webhook(req):
received = req.headers.get("X-Documenso-Secret", "")
expected = os.environ["DOCUMENSO_WEBHOOK_SECRET"]
return hmac.compare_digest(received, expected)
// Principle of least privilege with API keys
// Personal keys: only YOUR documents
// Team keys: all documents in the team
// Restrict document access by checking ownership
async function getDocumentSecure(documentId: number, userId: string) {
const doc = await client.documents.getV0(documentId);
// Verify the requesting user is the owner or a recipient
const isOwner = doc.userId === parseInt(userId);
const isRecipient = doc.recipients?.some(r => r.email === userEmail);
if (!isOwner && !isRecipient) {
throw new Error("Access denied: not authorized for this document");
}
return doc;
}
Self-hosted Documenso requires a .p12 signing certificate for legally valid digital signatures.
# Generate a self-signed certificate (development only)
openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes
openssl pkcs12 -export -out signing-cert.p12 -inkey key.pem -in cert.pem
# Mount into Docker container
docker run -v $(pwd)/signing-cert.p12:/opt/documenso/cert.p12 \
-e NEXT_PRIVATE_SIGNING_LOCAL_FILE_PATH=/opt/documenso/cert.p12 \
-e NEXT_PRIVATE_SIGNING_PASSPHRASE=your-passphrase \
documenso/documenso:latest
For production, use a certificate from a trusted CA (e.g., GlobalSign, DigiCert).
# Generate cryptographically secure secrets
openssl rand -hex 32 # NEXTAUTH_SECRET
openssl rand -hex 32 # NEXT_PRIVATE_ENCRYPTION_KEY
openssl rand -hex 32 # NEXT_PRIVATE_ENCRYPTION_SECONDARY_KEY
# Never reuse secrets across environments
# Never use default values in production
.env in .gitignore
openssl rand -hex 32
| Security Issue | Indicator | Response |
|---|---|---|
| Invalid API key | 401 errors | Rotate key immediately |
| Webhook spoofing | Invalid secret header | Reject request, alert team |
| Key exposed in git | GitHub secret scanning alert | Revoke key, rotate, audit access |
| Brute force | Many 401s from same IP | Rate limit by IP at reverse proxy |
For production deployment, see documenso-prod-checklist.