Security Guide
Essential security practices for building secure messaging applications with MsGine.
API Key Security
Never Expose Keys
API keys must never be exposed in:
- Client-side code: Browser JavaScript, mobile apps
- Version control: Git repositories, especially public ones
- Log files: Application logs, error messages
- Error responses: API error responses sent to clients
- Screenshots: Documentation, support tickets
Use Environment Variables
Store keys securely using environment variables:
// ❌ NEVER do this
const client = new MsGineClient({
apiKey: 'msgine_live_abc123...'
})
// ✅ Always use environment variables
const client = new MsGineClient({
apiKey: process.env.MSGINE_API_KEY!,
}).env File
# .env
MSGINE_API_KEY=your_api_key_here
MSGINE_WEBHOOK_SECRET=your_webhook_secret_hereImportant: Add .env to .gitignore:
# .gitignore
.env
.env.local
.env.*.localUse Secret Management Systems
For production environments, use dedicated secret management:
AWS Secrets Manager
import { SecretsManager } from '@aws-sdk/client-secrets-manager'
const client = new SecretsManager({ region: 'us-east-1' })
async function getApiKey(): Promise<string> {
const response = await client.getSecretValue({
SecretId: 'msgine/api-key'
})
return response.SecretString!
}Azure Key Vault
import { SecretClient } from '@azure/keyvault-secrets'
import { DefaultAzureCredential } from '@azure/identity'
const credential = new DefaultAzureCredential()
const client = new SecretClient(
'https://your-vault.vault.azure.net',
credential
)
async function getApiKey(): Promise<string> {
const secret = await client.getSecret('msgine-api-key')
return secret.value!
}Google Cloud Secret Manager
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
const client = new SecretManagerServiceClient()
async function getApiKey(): Promise<string> {
const [version] = await client.accessSecretVersion({
name: 'projects/my-project/secrets/msgine-api-key/versions/latest'
})
return version.payload!.data!.toString()
}Key Rotation
Rotate API keys regularly:
- Generate a new key in the Developer Dashboard
- Update your secret management system
- Deploy the new key to your application
- Verify the new key works
- Revoke the old key
Recommended rotation schedule:
- Production: Every 90 days
- Development: As needed
- Compromised: Immediately
Webhook Security
Verify Signatures
Always verify webhook signatures to prevent spoofing:
import crypto from 'crypto'
function verifyWebhookSignature(req: Request): boolean {
const signature = req.headers['x-msgine-signature'] as string
const secret = process.env.MSGINE_WEBHOOK_SECRET!
const payload = JSON.stringify(req.body)
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex')
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
)
}
app.post('/webhooks/msgine', (req, res) => {
if (!verifyWebhookSignature(req)) {
return res.status(401).json({ error: 'Invalid signature' })
}
// Process webhook
// ...
})Use HTTPS Only
Configure webhooks to use HTTPS URLs only:
// ❌ Insecure
callbackUrl: 'http://myapp.com/webhook'
// ✅ Secure
callbackUrl: 'https://myapp.com/webhook'MsGine will reject HTTP webhook URLs.
IP Whitelisting
Restrict webhook requests to MsGine's IP addresses:
const MSGINE_IPS = [
'52.89.214.238',
'34.212.75.30',
'54.218.53.128'
]
function isValidMsGineIP(ip: string): boolean {
return MSGINE_IPS.includes(ip)
}
app.post('/webhooks/msgine', (req, res) => {
const clientIP = req.ip
if (!isValidMsGineIP(clientIP)) {
return res.status(403).json({ error: 'Forbidden' })
}
// Process webhook
// ...
})Implement Idempotency
Prevent duplicate webhook processing:
const processedWebhooks = new Set<string>()
app.post('/webhooks/msgine', (req, res) => {
const webhookId = req.body.id
if (processedWebhooks.has(webhookId)) {
console.log('Duplicate webhook, skipping')
return res.status(200).json({ received: true })
}
processedWebhooks.add(webhookId)
// Process webhook
// ...
})Input Validation
Validate Phone Numbers
Always validate phone numbers before sending:
function validatePhoneNumber(phone: string): boolean {
// E.164 format: +[1-15 digits]
const e164Regex = /^\+[1-9]\d{1,14}$/
if (!e164Regex.test(phone)) {
throw new Error('Phone number must be in E.164 format')
}
return true
}
async function sendSms(to: string, message: string) {
validatePhoneNumber(to)
return await client.sms.send({ to, message })
}Sanitize Message Content
Remove potentially harmful content:
function sanitizeMessage(message: string): string {
// Remove control characters
let sanitized = message.replace(/[\x00-\x1F\x7F]/g, '')
// Trim whitespace
sanitized = sanitized.trim()
// Validate length
if (sanitized.length === 0) {
throw new Error('Message cannot be empty')
}
if (sanitized.length > 1000) {
throw new Error('Message exceeds maximum length of 1000 characters')
}
return sanitized
}
async function sendSms(to: string, message: string) {
const sanitizedMessage = sanitizeMessage(message)
return await client.sms.send({ to, message: sanitizedMessage })
}Prevent Injection Attacks
Validate and escape user input:
function escapeMessage(message: string): string {
// Escape potentially dangerous characters
return message
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}Rate Limiting
Implement Application-Level Rate Limiting
Protect your application from abuse:
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Limit each IP to 100 requests per window
message: 'Too many requests, please try again later'
})
app.post('/api/send-sms', limiter, async (req, res) => {
// Handle SMS sending
})Per-User Rate Limiting
Implement per-user limits:
import Redis from 'ioredis'
const redis = new Redis()
async function checkUserRateLimit(userId: string): Promise<boolean> {
const key = `rate_limit:${userId}`
const count = await redis.incr(key)
if (count === 1) {
await redis.expire(key, 3600) // 1 hour
}
const limit = 100 // 100 messages per hour per user
return count <= limit
}
app.post('/api/send-sms', async (req, res) => {
const userId = req.user.id
if (!await checkUserRateLimit(userId)) {
return res.status(429).json({ error: 'Rate limit exceeded' })
}
// Send SMS
})Data Protection
Encrypt Sensitive Data
Encrypt phone numbers and message content in your database:
import crypto from 'crypto'
const ENCRYPTION_KEY = process.env.ENCRYPTION_KEY! // 32 bytes
const IV_LENGTH = 16
function encrypt(text: string): string {
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv(
'aes-256-cbc',
Buffer.from(ENCRYPTION_KEY),
iv
)
let encrypted = cipher.update(text)
encrypted = Buffer.concat([encrypted, cipher.final()])
return iv.toString('hex') + ':' + encrypted.toString('hex')
}
function decrypt(text: string): string {
const parts = text.split(':')
const iv = Buffer.from(parts[0], 'hex')
const encrypted = Buffer.from(parts[1], 'hex')
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
Buffer.from(ENCRYPTION_KEY),
iv
)
let decrypted = decipher.update(encrypted)
decrypted = Buffer.concat([decrypted, decipher.final()])
return decrypted.toString()
}Implement Data Retention Policies
Automatically delete old message data:
async function deleteOldMessages() {
const thirtyDaysAgo = new Date()
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
await db.messages.deleteMany({
createdAt: { $lt: thirtyDaysAgo }
})
}
// Run daily
setInterval(deleteOldMessages, 24 * 60 * 60 * 1000)Compliance
GDPR Compliance
For EU users:
- Consent: Obtain explicit consent before sending messages
- Right to erasure: Allow users to delete their data
- Data portability: Provide user data exports
- Privacy policy: Clearly explain data usage
async function checkConsent(userId: string): Promise<boolean> {
const consent = await db.consents.findOne({
userId,
consentType: 'sms',
consentGiven: true
})
return !!consent
}
async function sendSms(userId: string, message: string) {
if (!await checkConsent(userId)) {
throw new Error('User has not consented to SMS messages')
}
const user = await db.users.findById(userId)
return await client.sms.send({
to: user.phone,
message
})
}TCPA Compliance (US)
For US users:
- Prior express consent: Get written consent
- Opt-out: Provide easy opt-out mechanism
- Hours: Don't send between 9 PM - 8 AM
- Identification: Identify your organization
function isValidSendingTime(): boolean {
const hour = new Date().getHours()
return hour >= 8 && hour < 21 // 8 AM - 9 PM
}
async function sendSmsCompliant(to: string, message: string) {
if (!isValidSendingTime()) {
throw new Error('Cannot send SMS outside 8 AM - 9 PM')
}
// Add organization identifier
const compliantMessage = `${message}\n\nReply STOP to opt out`
return await client.sms.send({ to, message: compliantMessage })
}Monitoring and Alerting
Log Security Events
Log all security-related events:
import winston from 'winston'
const logger = winston.createLogger({
level: 'info',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'security.log' })
]
})
// Log failed authentication
logger.warn('Webhook signature verification failed', {
ip: req.ip,
timestamp: new Date().toISOString()
})Incident Response
Have a Security Incident Plan
- Detect: Monitor for suspicious activity
- Contain: Revoke compromised keys immediately
- Investigate: Review logs to understand the breach
- Recover: Generate new keys and update systems
- Learn: Document and improve security measures
Key Compromise Response
If a key is compromised:
async function handleKeyCompromise() {
// 1. Revoke compromised key immediately
await revokeKey(compromisedKey)
// 2. Generate new key
const newKey = await generateNewKey()
// 3. Update secret management
await updateSecret('msgine-api-key', newKey)
// 4. Alert team
await alertTeam('API key compromised and rotated')
// 5. Review logs
await reviewAuditLogs(compromisedKey)
}Next Steps
- Best Practices - General best practices
- Error Handling - Handle errors securely
- Webhooks - Secure webhook implementation