Skip to main content

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

Critical Security Rule

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:

.env (never commit this file)
SAVVY_CLIENT_ID=your_client_id
SAVVY_CLIENT_SECRET=your_client_secret
SAVVY_WEBHOOK_SECRET=your_webhook_secret
Accessing credentials
const clientId = process.env.SAVVY_CLIENT_ID;
const clientSecret = process.env.SAVVY_CLIENT_SECRET;

Secret Management Services

For production, use a secrets management service:

ServiceUse Case
AWS Secrets ManagerAWS deployments
HashiCorp VaultMulti-cloud, on-premise
Azure Key VaultAzure deployments
Google Secret ManagerGCP deployments
AWS Secrets Manager Example
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

  1. Never store tokens in localStorage - Vulnerable to XSS attacks
  2. Use httpOnly cookies - For web applications
  3. Secure memory storage - For mobile applications
  4. Short expiration - Tokens expire in 1 hour
Secure Cookie Storage
// 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

Automatic Token Refresh
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 TypeStorageTransmissionLogging
SSN (last 4)EncryptedHTTPS onlyNever log
Credit ScoreEncryptedHTTPS onlyMask in logs
User EmailEncryptedHTTPS onlyHash in logs
User NameEncryptedHTTPS onlyAllowed
PII Logging - What NOT to do
// ❌ 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:

Webhook Verification
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

Secure Webhook Handler
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:

Rate Limiter
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:

Input Validation
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:

  1. Contact SavvyMoney support to set up allowlisting
  2. Provide your static IP addresses or CIDR ranges
  3. Use a NAT gateway for consistent outbound IPs

Network Security

Security Group Configuration
# 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:

mTLS Configuration
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:

Audit Logger
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:

EventThresholdAction
Failed auth attempts5 in 5 minutesAlert + temporary block
Unusual API volume2x normalAlert
Webhook failures3 consecutiveAlert
Invalid signaturesAnyAlert + 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