HubSpot Security Basics
Overview
Security best practices for HubSpot private app tokens, OAuth scopes, webhook signature verification, and secret management.
Prerequisites
- HubSpot private app or OAuth app configured
- Understanding of environment variables and secret management
Instructions
Step 1: Least-Privilege Scopes
Only request the scopes your integration actually uses:
| Use Case | Required Scopes |
|----------|----------------|
| Read contacts | crm.objects.contacts.read |
| Write contacts | crm.objects.contacts.read, crm.objects.contacts.write |
| Read/write deals | crm.objects.deals.read, crm.objects.deals.write |
| Marketing emails | content |
| Forms | forms |
| Contact lists | crm.lists.read, crm.lists.write |
| Properties | crm.schemas.contacts.read |
| Custom objects | crm.objects.custom.read, crm.objects.custom.write, crm.schemas.custom.read |
| Webhooks | automation |
Never use: Do not grant all scopes. If you regenerate a private app token, the old token is immediately revoked.
Step 2: Token Storage
# .env (NEVER commit)
HUBSPOT_ACCESS_TOKEN=pat-na1-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
HUBSPOT_WEBHOOK_SECRET=your-webhook-secret
# .gitignore
.env
.env.local
.env.*.local
// Validate token is present at startup
function validateConfig(): void {
if (!process.env.HUBSPOT_ACCESS_TOKEN) {
throw new Error('HUBSPOT_ACCESS_TOKEN is required. See .env.example');
}
// Never log the token
console.log('HubSpot: Token configured', {
prefix: process.env.HUBSPOT_ACCESS_TOKEN.substring(0, 8) + '...',
});
}
Step 3: Webhook Signature Verification (v3)
HubSpot sends webhooks with signature verification headers:
import crypto from 'crypto';
import express from 'express';
// HubSpot v3 signature verification
// Header: X-HubSpot-Signature-v3
function verifyHubSpotSignatureV3(
requestBody: string,
signature: string,
timestamp: string,
clientSecret: string,
requestUri: string,
method: string = 'POST'
): boolean {
// Reject if timestamp is older than 5 minutes (replay protection)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
console.warn('HubSpot webhook timestamp too old');
return false;
}
// v3: HMAC SHA-256 of method + URI + body + timestamp
const sourceString = `${method}${requestUri}${requestBody}${timestamp}`;
const expectedSignature = crypto
.createHmac('sha256', clientSecret)
.update(sourceString)
.digest('base64');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express middleware
const webhookRouter = express.Router();
webhookRouter.post('/hubspot',
express.raw({ type: 'application/json' }),
(req, res) => {
const signature = req.headers['x-hubspot-signature-v3'] as string;
const timestamp = req.headers['x-hubspot-request-timestamp'] as string;
const requestUri = `https://${req.headers.host}${req.originalUrl}`;
if (!verifyHubSpotSignatureV3(
req.body.toString(), signature, timestamp,
process.env.HUBSPOT_WEBHOOK_SECRET!, requestUri
)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const events = JSON.parse(req.body.toString());
// Process events...
res.status(200).json({ received: true });
}
);
Step 4: Token Rotation Procedure
# 1. Generate new token in HubSpot
# Settings > Integrations > Private Apps > [Your App] > Auth tab
# Click "Rotate token" (old token revoked immediately)
# 2. Update in your secret manager
# AWS Secrets Manager
aws secretsmanager update-secret --secret-id hubspot/production \
--secret-string '{"access_token":"pat-na1-NEW_TOKEN"}'
# GCP Secret Manager
echo -n "pat-na1-NEW_TOKEN" | gcloud secrets versions add hubspot-token --data-file=-
# 3. Restart/redeploy your application to pick up new token
# 4. Verify new token works
curl -s https://api.hubapi.com/crm/v3/objects/contacts?limit=1 \
-H "Authorization: Bearer $HUBSPOT_ACCESS_TOKEN" | jq .status
Step 5: Git Secret Scanning
# .github/workflows/secret-scan.yml
name: Secret Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Check for HubSpot tokens
run: |
if grep -rE "pat-[a-z]{2}[0-9]-[a-f0-9-]{36}" --include="*.ts" --include="*.js" --include="*.json" .; then
echo "ERROR: HubSpot access token found in source code"
exit 1
fi
Output
- Minimal scopes configured per use case
- Tokens stored in environment variables, never in code
- Webhook signatures verified with replay protection
- Token rotation procedure documented
- CI scanning for leaked tokens
Error Handling
| Security Issue | Detection | Mitigation |
|----------------|-----------|------------|
| Token in git history | git log -p --all -S "pat-na1" | Rotate token immediately |
| Excessive scopes | Audit in Settings > Private Apps | Remove unneeded scopes |
| Unverified webhooks | Security audit | Add signature verification |
| Token never rotated | Track creation date | Schedule quarterly rotation |
Resources
Next Steps
For production deployment, see hubspot-prod-checklist.