From hubspot-pack
Apply HubSpot security best practices for tokens, scopes, and webhook verification. Use when securing private app tokens, implementing least privilege scopes, or validating HubSpot webhook signatures. Trigger with phrases like "hubspot security", "hubspot token rotation", "secure hubspot", "hubspot scopes", "hubspot webhook verify".
npx claudepluginhub flight505/skill-forge --plugin hubspot-packThis skill is limited to using the following tools:
Security best practices for HubSpot private app tokens, OAuth scopes, webhook signature verification, and secret management.
Guides Next.js Cache Components and Partial Prerendering (PPR): 'use cache' directives, cacheLife(), cacheTag(), revalidateTag() for caching, invalidation, static/dynamic optimization. Auto-activates on cacheComponents: true.
Guides building MCP servers enabling LLMs to interact with external services via tools. Covers best practices, TypeScript/Node (MCP SDK), Python (FastMCP).
Share bugs, ideas, or general feedback.
Security best practices for HubSpot private app tokens, OAuth scopes, webhook signature verification, and secret management.
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.
# .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) + '...',
});
}
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 });
}
);
# 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
# .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
| 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 |
For production deployment, see hubspot-prod-checklist.