Security Best Practices

    Essential security practices for implementing the Smartlinks API safely and securely

    Security Best Practices

    Follow these security guidelines to keep your Smartlinks integration secure.

    API Key Management

    Never Expose API Keys

    Bad - Exposing in Client Code:

    // NEVER DO THIS
    const apiKey = 'sk_live_abc123...'; // Hardcoded API key
    fetch('https://api.smartlinks.io/v1/links', {
      headers: { 'Authorization': `Bearer ${apiKey}` }
    });
    

    Good - Server-side Only:

    // Server-side code (Node.js)
    const apiKey = process.env.SMARTLINKS_API_KEY;
    
    app.post('/api/create-link', async (req, res) => {
      const response = await fetch('https://api.smartlinks.io/v1/links', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify(req.body)
      });
      
      const data = await response.json();
      res.json(data);
    });
    

    Environment Variables

    Store API keys securely:

    # .env file (add to .gitignore!)
    SMARTLINKS_API_KEY=sk_live_your_key_here
    SMARTLINKS_WEBHOOK_SECRET=whsec_your_secret_here
    
    // Load environment variables
    require('dotenv').config();
    
    const apiKey = process.env.SMARTLINKS_API_KEY;
    const webhookSecret = process.env.SMARTLINKS_WEBHOOK_SECRET;
    

    Key Rotation

    Rotate API keys regularly:

    // Support multiple API keys during rotation
    const primaryKey = process.env.SMARTLINKS_API_KEY_PRIMARY;
    const secondaryKey = process.env.SMARTLINKS_API_KEY_SECONDARY;
    
    async function makeRequestWithFallback(endpoint, options) {
      try {
        // Try primary key first
        return await makeRequest(endpoint, primaryKey, options);
      } catch (error) {
        if (error.status === 401) {
          // Fallback to secondary key
          return await makeRequest(endpoint, secondaryKey, options);
        }
        throw error;
      }
    }
    

    Rate Limiting

    Implement client-side rate limiting:

    class RateLimiter {
      constructor(maxRequests, windowMs) {
        this.maxRequests = maxRequests;
        this.windowMs = windowMs;
        this.requests = [];
      }
      
      async acquire() {
        const now = Date.now();
        this.requests = this.requests.filter(
          time => now - time < this.windowMs
        );
        
        if (this.requests.length >= this.maxRequests) {
          const oldestRequest = this.requests[0];
          const waitTime = this.windowMs - (now - oldestRequest);
          await new Promise(resolve => setTimeout(resolve, waitTime));
          return this.acquire();
        }
        
        this.requests.push(now);
      }
    }
    
    // Usage: 100 requests per minute
    const limiter = new RateLimiter(100, 60000);
    
    async function createLink(url) {
      await limiter.acquire();
      return fetch('https://api.smartlinks.io/v1/links', {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${apiKey}`,
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({ destination_url: url })
      });
    }
    

    Input Validation

    Always validate and sanitize user input:

    const validator = require('validator');
    
    function validateLinkInput(input) {
      const errors = [];
      
      // Validate destination URL
      if (!input.destination_url) {
        errors.push('Destination URL is required');
      } else if (!validator.isURL(input.destination_url, {
        protocols: ['http', 'https'],
        require_protocol: true
      })) {
        errors.push('Invalid destination URL');
      }
      
      // Validate custom slug
      if (input.custom_slug) {
        if (!/^[a-zA-Z0-9-_]+$/.test(input.custom_slug)) {
          errors.push('Custom slug can only contain letters, numbers, hyphens, and underscores');
        }
        if (input.custom_slug.length > 50) {
          errors.push('Custom slug must be 50 characters or less');
        }
      }
      
      return errors;
    }
    
    // Usage
    app.post('/api/create-link', (req, res) => {
      const errors = validateLinkInput(req.body);
      
      if (errors.length > 0) {
        return res.status(400).json({ errors });
      }
      
      // Proceed with link creation
      createLink(req.body)
        .then(link => res.json(link))
        .catch(error => res.status(500).json({ error: error.message }));
    });
    

    Webhook Security

    Verify Signatures

    Always verify webhook signatures:

    const crypto = require('crypto');
    
    function verifyWebhookSignature(payload, signature, secret) {
      const hmac = crypto.createHmac('sha256', secret);
      const digest = hmac.update(JSON.stringify(payload)).digest('hex');
      
      // Use timing-safe comparison
      return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(digest)
      );
    }
    
    app.post('/webhooks/smartlinks', express.json(), (req, res) => {
      const signature = req.headers['x-smartlinks-signature'];
      const secret = process.env.WEBHOOK_SECRET;
      
      if (!verifyWebhookSignature(req.body, signature, secret)) {
        console.error('Invalid webhook signature');
        return res.status(401).json({ error: 'Unauthorized' });
      }
      
      handleWebhook(req.body);
      res.status(200).json({ received: true });
    });
    

    Idempotency

    Handle duplicate webhook events:

    const processedEvents = new Set();
    
    async function handleWebhook(event) {
      // Check if already processed
      if (processedEvents.has(event.id)) {
        console.log('Duplicate event, skipping:', event.id);
        return;
      }
      
      // Process the event
      await processEvent(event);
      
      // Mark as processed
      processedEvents.add(event.id);
      
      // Clean up old events (older than 24 hours)
      cleanupOldEvents();
    }
    

    HTTPS Only

    Always use HTTPS for API requests:

    const BASE_URL = 'https://api.smartlinks.io/v1'; // Always HTTPS
    
    // Reject non-HTTPS URLs
    function ensureHttps(url) {
      const parsed = new URL(url);
      if (parsed.protocol !== 'https:') {
        throw new Error('Only HTTPS URLs are allowed');
      }
      return url;
    }
    

    Error Handling

    Don't expose sensitive information in errors:

    // ❌ Bad - Exposes sensitive info
    app.post('/api/create-link', async (req, res) => {
      try {
        const link = await createLink(req.body);
        res.json(link);
      } catch (error) {
        res.status(500).json({ error: error.message }); // May expose API key or internal details
      }
    });
    
    // ✅ Good - Generic error message
    app.post('/api/create-link', async (req, res) => {
      try {
        const link = await createLink(req.body);
        res.json(link);
      } catch (error) {
        console.error('Link creation error:', error); // Log internally
        res.status(500).json({ 
          error: 'Failed to create link. Please try again.' 
        });
      }
    });
    

    Monitoring & Logging

    Log security-relevant events:

    function logSecurityEvent(event, details) {
      const log = {
        timestamp: new Date().toISOString(),
        event: event,
        details: details,
        ip: details.ip,
        userAgent: details.userAgent
      };
      
      // Send to logging service
      logger.security(log);
      
      // Alert on suspicious activity
      if (event === 'invalid_signature' || event === 'rate_limit_exceeded') {
        alertSecurityTeam(log);
      }
    }
    

    Security Checklist

    • ✅ Store API keys in environment variables
    • ✅ Never commit secrets to version control
    • ✅ Use HTTPS for all API requests
    • ✅ Verify webhook signatures
    • ✅ Implement rate limiting
    • ✅ Validate all user input
    • ✅ Use idempotency keys for webhooks
    • ✅ Rotate API keys regularly
    • ✅ Log security events
    • ✅ Handle errors securely
    • ❌ Never expose API keys in client-side code
    • ❌ Never log sensitive data

    Next Steps