Security Guide
Essential security practices for building secure messaging applications with MsGine.
API Token Security
Never Expose Tokens
API tokens 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 tokens securely using environment variables:
// ❌ NEVER do this
const client = new MsGineClient({
apiToken: 'msgine_live_abc123...'
})
// ✅ Always use environment variables
const client = new MsGineClient({
apiToken: process.env.MSGINE_API_TOKEN!
}).env File
# .env
MSGINE_API_TOKEN=your_api_token_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 getApiToken(): Promise<string> {
const response = await client.getSecretValue({
SecretId: 'msgine/api-token'
})
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 getApiToken(): Promise<string> {
const secret = await client.getSecret('msgine-api-token')
return secret.value!
}Google Cloud Secret Manager
import { SecretManagerServiceClient } from '@google-cloud/secret-manager'
const client = new SecretManagerServiceClient()
async function getApiToken(): Promise<string> {
const [version] = await client.accessSecretVersion({
name: 'projects/my-project/secrets/msgine-api-token/versions/latest'
})
return version.payload!.data!.toString()
}Token Rotation
Rotate API tokens regularly:
- Generate a new token in the Developer Dashboard
- Update your secret management system
- Deploy the new token to your application
- Verify the new token works
- Revoke the old token
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.sendSms({ 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 > 1600) {
throw new Error('Message exceeds maximum length of 1600 characters')
}
return sanitized
}
async function sendSms(to: string, message: string) {
const sanitizedMessage = sanitizeMessage(message)
return await client.sendSms({ 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()
}Hash Phone Numbers for Lookups
Hash phone numbers when storing for lookup:
import crypto from 'crypto'
function hashPhoneNumber(phone: string): string {
return crypto
.createHash('sha256')
.update(phone)
.digest('hex')
}
// Store hashed phone number
const hash = hashPhoneNumber('+256701521269')
await db.users.create({
phoneHash: hash,
// ... other fields
})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
interface UserConsent {
userId: string
consentType: 'sms' | 'email'
consentGiven: boolean
consentDate: Date
}
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.sendSms({
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.sendSms({ 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()
})
// Log unusual activity
logger.warn('High volume of messages from user', {
userId,
messageCount: count,
timestamp: new Date().toISOString()
})Set Up Alerts
Configure alerts for security incidents:
async function alertSecurityIncident(type: string, details: any) {
// Send to monitoring service
await monitoringService.sendAlert({
severity: 'high',
type,
details,
timestamp: new Date()
})
// Notify security team
await sendEmail({
to: 'security@example.com',
subject: `Security Alert: ${type}`,
body: JSON.stringify(details, null, 2)
})
}
app.post('/webhooks/msgine', (req, res) => {
if (!verifyWebhookSignature(req)) {
alertSecurityIncident('Invalid webhook signature', {
ip: req.ip,
headers: req.headers
})
return res.status(401).json({ error: 'Invalid signature' })
}
// Process webhook
})Incident Response
Have a Security Incident Plan
- Detect: Monitor for suspicious activity
- Contain: Revoke compromised tokens immediately
- Investigate: Review logs to understand the breach
- Recover: Generate new tokens and update systems
- Learn: Document and improve security measures
Token Compromise Response
If a token is compromised:
async function handleTokenCompromise() {
// 1. Revoke compromised token immediately
await revokeToken(compromisedToken)
// 2. Generate new token
const newToken = await generateNewToken()
// 3. Update secret management
await updateSecret('msgine-api-token', newToken)
// 4. Alert team
await alertTeam('Token compromised and rotated')
// 5. Review logs
await reviewAuditLogs(compromisedToken)
// 6. Notify affected users if necessary
if (dataExposed) {
await notifyAffectedUsers()
}
}Next Steps
- Best Practices - General best practices
- Error Handling - Handle errors securely
- Webhooks - Secure webhook implementation