Security Best Practices
Protecting user data is critical when integrating with SavvyMoney. Follow these security best practices to ensure a secure implementation.
Credential Management
Never Expose Secrets
Never expose client credentials in client-side code, version control, or logs.
❌ Don't do this:
// NEVER put credentials in frontend code
const client = new SavvyMoney({
clientId: 'sm_live_abc123', // Exposed!
clientSecret: 'sk_live_xyz789' // Exposed!
});
✅ Do this instead:
// Keep credentials server-side only
// Frontend requests token from your backend
const response = await fetch('/api/savvymoney/token');
const { userToken } = await response.json();
Environment Variables
Store credentials in environment variables:
SAVVY_CLIENT_ID=your_client_id
SAVVY_CLIENT_SECRET=your_client_secret
SAVVY_WEBHOOK_SECRET=your_webhook_secret
const clientId = process.env.SAVVY_CLIENT_ID;
const clientSecret = process.env.SAVVY_CLIENT_SECRET;
Secret Management Services
For production, use a secrets management service:
| Service | Use Case |
|---|---|
| AWS Secrets Manager | AWS deployments |
| HashiCorp Vault | Multi-cloud, on-premise |
| Azure Key Vault | Azure deployments |
| Google Secret Manager | GCP deployments |
import { SecretsManagerClient, GetSecretValueCommand } from '@aws-sdk/client-secrets-manager';
const client = new SecretsManagerClient({ region: 'us-east-1' });
async function getCredentials() {
const command = new GetSecretValueCommand({
SecretId: 'savvymoney/api-credentials'
});
const response = await client.send(command);
return JSON.parse(response.SecretString);
}
Token Security
Token Handling
- Never store tokens in localStorage - Vulnerable to XSS attacks
- Use httpOnly cookies - For web applications
- Secure memory storage - For mobile applications
- Short expiration - Tokens expire in 1 hour
// Set token in httpOnly cookie (server-side)
res.cookie('savvy_token', userToken, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 3600000 // 1 hour
});
Token Refresh Strategy
class SecureTokenManager {
private token: string | null = null;
private expiresAt: number = 0;
async getToken(): Promise<string> {
// Refresh 5 minutes before expiration
if (!this.token || Date.now() > this.expiresAt - 300000) {
await this.refreshToken();
}
return this.token!;
}
private async refreshToken(): Promise<void> {
const response = await fetch('/api/auth/savvymoney-token', {
method: 'POST',
credentials: 'include'
});
if (!response.ok) {
throw new Error('Token refresh failed');
}
const data = await response.json();
this.token = data.token;
this.expiresAt = Date.now() + (data.expiresIn * 1000);
}
}
Data Protection
PII Handling
Credit data contains Personally Identifiable Information (PII). Follow these guidelines:
| Data Type | Storage | Transmission | Logging |
|---|---|---|---|
| SSN (last 4) | Encrypted | HTTPS only | Never log |
| Credit Score | Encrypted | HTTPS only | Mask in logs |
| User Email | Encrypted | HTTPS only | Hash in logs |
| User Name | Encrypted | HTTPS only | Allowed |
// ❌ Never log PII
console.log(`User ${user.email} has score ${score}`);
console.log(`SSN: ${user.ssn}`);
// ✅ Use masked/hashed values
console.log(`User ${hashEmail(user.email)} accessed score`);
console.log(`Score check for user_id: ${user.id}`);
Data Encryption
At Rest:
// Encrypt sensitive data before storing
import { createCipheriv, randomBytes } from 'crypto';
function encryptData(data: string, key: Buffer): string {
const iv = randomBytes(16);
const cipher = createCipheriv('aes-256-gcm', key, iv);
let encrypted = cipher.update(data, 'utf8', 'hex');
encrypted += cipher.final('hex');
const authTag = cipher.getAuthTag();
return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}
In Transit:
- All API calls use HTTPS (TLS 1.2+)
- Certificate pinning for mobile apps
- HSTS headers for web applications
Data Retention
Follow data retention requirements:
// Implement data retention policies
async function cleanupUserData(userId: string) {
// Delete cached credit data older than 30 days
await cache.deletePattern(`credit:${userId}:*`, {
olderThan: 30 * 24 * 60 * 60 * 1000
});
// Audit log retention (keep for compliance)
// Do NOT delete audit logs
}
Webhook Security
Signature Verification
Always verify webhook signatures:
import crypto from 'crypto';
function verifyWebhook(payload: string, signature: string, secret: string): boolean {
const elements = signature.split(',');
const timestamp = elements.find(e => e.startsWith('t='))?.slice(2);
const receivedSig = elements.find(e => e.startsWith('v1='))?.slice(3);
if (!timestamp || !receivedSig) {
return false;
}
// Prevent replay attacks - reject old webhooks
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) { // 5 minutes
return false;
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const expectedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(receivedSig),
Buffer.from(expectedSig)
);
}
Webhook Endpoint Security
app.post('/webhooks/savvymoney', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-savvymoney-signature'];
// 1. Verify signature
if (!verifyWebhook(req.body.toString(), signature, process.env.WEBHOOK_SECRET)) {
console.warn('Invalid webhook signature');
return res.status(401).json({ error: 'Invalid signature' });
}
// 2. Parse and validate payload
const payload = JSON.parse(req.body);
// 3. Idempotency check - prevent duplicate processing
const eventId = payload.id;
if (await isEventProcessed(eventId)) {
return res.status(200).json({ received: true, duplicate: true });
}
// 4. Process asynchronously
processWebhookAsync(payload);
// 5. Acknowledge immediately
res.status(200).json({ received: true });
});
API Security
Rate Limiting
Implement client-side rate limiting to avoid hitting API limits:
class RateLimiter {
private requests: number[] = [];
private limit: number;
private window: number;
constructor(limit: number, windowMs: number) {
this.limit = limit;
this.window = windowMs;
}
async throttle(): Promise<void> {
const now = Date.now();
this.requests = this.requests.filter(t => t > now - this.window);
if (this.requests.length >= this.limit) {
const waitTime = this.requests[0] + this.window - now;
await new Promise(resolve => setTimeout(resolve, waitTime));
}
this.requests.push(now);
}
}
// Usage
const limiter = new RateLimiter(100, 60000); // 100 requests per minute
async function makeApiCall() {
await limiter.throttle();
return fetch('https://api.savvymoney.com/v1/...');
}
Input Validation
Validate all inputs before sending to the API:
import { z } from 'zod';
const UserEnrollmentSchema = z.object({
externalUserId: z.string().min(1).max(255),
email: z.string().email(),
firstName: z.string().min(1).max(100),
lastName: z.string().min(1).max(100),
ssn: z.string().regex(/^\d{4}$/, 'Must be last 4 digits'),
dateOfBirth: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
phone: z.string().optional()
});
function enrollUser(data: unknown) {
// Validate input
const validated = UserEnrollmentSchema.parse(data);
// Proceed with API call
return api.enrollUser(validated);
}
Infrastructure Security
IP Allowlisting
For production environments, configure IP allowlisting:
- Contact SavvyMoney support to set up allowlisting
- Provide your static IP addresses or CIDR ranges
- Use a NAT gateway for consistent outbound IPs
Network Security
# Allow only HTTPS outbound to SavvyMoney
outbound_rules:
- protocol: tcp
port: 443
destination: api.savvymoney.com
# Restrict webhook endpoint access
inbound_rules:
- protocol: tcp
port: 443
source: savvymoney-webhook-ips
mTLS (Mutual TLS)
For enhanced security, implement mutual TLS:
import https from 'https';
import fs from 'fs';
const agent = new https.Agent({
cert: fs.readFileSync('client-cert.pem'),
key: fs.readFileSync('client-key.pem'),
ca: fs.readFileSync('savvymoney-ca.pem'),
rejectUnauthorized: true
});
fetch('https://api.savvymoney.com/v1/users', { agent });
Audit & Compliance
Audit Logging
Log all security-relevant events:
interface AuditEvent {
timestamp: string;
eventType: string;
userId?: string;
action: string;
resource: string;
outcome: 'success' | 'failure';
ipAddress: string;
userAgent: string;
details?: Record<string, unknown>;
}
function logAuditEvent(event: AuditEvent) {
// Never log PII in audit events
const sanitizedEvent = {
...event,
userId: event.userId ? hashUserId(event.userId) : undefined
};
auditLogger.info(JSON.stringify(sanitizedEvent));
}
// Usage
logAuditEvent({
timestamp: new Date().toISOString(),
eventType: 'CREDIT_SCORE_ACCESS',
userId: user.id,
action: 'READ',
resource: 'credit-score',
outcome: 'success',
ipAddress: req.ip,
userAgent: req.headers['user-agent']
});
Security Monitoring
Set up alerts for suspicious activity:
| Event | Threshold | Action |
|---|---|---|
| Failed auth attempts | 5 in 5 minutes | Alert + temporary block |
| Unusual API volume | 2x normal | Alert |
| Webhook failures | 3 consecutive | Alert |
| Invalid signatures | Any | Alert + investigate |
Security Checklist
Before going to production, verify:
- Credentials stored in secure vault (not code/config)
- All API calls use HTTPS
- Webhook signatures verified
- PII encrypted at rest
- PII masked in logs
- Rate limiting implemented
- Input validation on all endpoints
- Audit logging enabled
- Security monitoring configured
- IP allowlisting configured (if applicable)
- Data retention policies implemented
- Incident response plan documented
Next Steps
- Review Error Handling for secure error responses
- Implement Performance optimizations
- Configure Webhooks securely