Skip to content

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:

typescript
// ❌ 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

bash
# .env
MSGINE_API_TOKEN=your_api_token_here
MSGINE_WEBHOOK_SECRET=your_webhook_secret_here

Important: Add .env to .gitignore:

bash
# .gitignore
.env
.env.local
.env.*.local

Use Secret Management Systems

For production environments, use dedicated secret management:

AWS Secrets Manager

typescript
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

typescript
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

typescript
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:

  1. Generate a new token in the Developer Dashboard
  2. Update your secret management system
  3. Deploy the new token to your application
  4. Verify the new token works
  5. 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:

typescript
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:

typescript
// ❌ 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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
function escapeMessage(message: string): string {
  // Escape potentially dangerous characters
  return message
    .replace(/&/g, '&amp;')
    .replace(/</g, '&lt;')
    .replace(/>/g, '&gt;')
    .replace(/"/g, '&quot;')
    .replace(/'/g, '&#x27;')
}

Rate Limiting

Implement Application-Level Rate Limiting

Protect your application from abuse:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

typescript
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:

  1. Consent: Obtain explicit consent before sending messages
  2. Right to erasure: Allow users to delete their data
  3. Data portability: Provide user data exports
  4. Privacy policy: Clearly explain data usage
typescript
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:

  1. Prior express consent: Get written consent
  2. Opt-out: Provide easy opt-out mechanism
  3. Hours: Don't send between 9 PM - 8 AM
  4. Identification: Identify your organization
typescript
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:

typescript
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:

typescript
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

  1. Detect: Monitor for suspicious activity
  2. Contain: Revoke compromised tokens immediately
  3. Investigate: Review logs to understand the breach
  4. Recover: Generate new tokens and update systems
  5. Learn: Document and improve security measures

Token Compromise Response

If a token is compromised:

typescript
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

Released under the MIT License.