Skip to content

Security Best Practices

Follow these best practices to ensure your Flowless integration is secure and production-ready.


Bridge Secret Management

Never Expose Your Bridge Secret

DANGER

Critical Security Rule Never commit your Bridge Secret to version control or expose it in client-side code!

✅ DO

bash
# .env file (add to .gitignore)
BRIDGE_SECRET=bridge_secret_abc123xyz789
typescript
// Backend only
const bridgeSecret = process.env.BRIDGE_SECRET;

❌ DON'T

typescript
// NEVER do this!
const bridgeSecret = 'bridge_secret_abc123xyz789'; // Hardcoded
javascript
// NEVER in frontend!
const response = await fetch('/bridge/validate', {
  headers: { 'X-Bridge-Secret': 'bridge_secret_abc123xyz789' }
});

Rotate Secrets Regularly

  • Frequency: Every 90 days minimum
  • After breach: Immediately
  • When employee leaves: Within 24 hours

Session Security

Use HTTPS Only

WARNING

Always use HTTPS in production. Never send session IDs over HTTP.

typescript
// ✅ Good
const FLOWLESS_URL = 'https://your-instance.pubflow.com';

// ❌ Bad
const FLOWLESS_URL = 'http://your-instance.pubflow.com';

Store Sessions Securely

Frontend Storage

typescript
// ✅ Best: HttpOnly cookies (if possible)
// Set by your backend
res.cookie('session_id', sessionId, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict',
  maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
});

// ✅ Good: localStorage (for SPAs)
localStorage.setItem('session_id', sessionId);

// ❌ Avoid: sessionStorage (lost on tab close)
sessionStorage.setItem('session_id', sessionId);

Mobile Apps

typescript
// React Native - Use AsyncStorage
import AsyncStorage from '@react-native-async-storage/async-storage';

await AsyncStorage.setItem('session_id', sessionId);

Clear Sessions on Logout

typescript
async function logout() {
  const sessionId = localStorage.getItem('session_id');
  
  // 1. Call Flowless logout
  await fetch(`${FLOWLESS_URL}/auth/logout`, {
    method: 'POST',
    headers: { 'X-Session-ID': sessionId },
  });
  
  // 2. Clear local storage
  localStorage.removeItem('session_id');
  localStorage.removeItem('user_data');
  
  // 3. Redirect to login
  window.location.href = '/login';
}

Validation Mode Selection

Choose the Right Mode

typescript
// Low-security apps (blogs, public content)
VALIDATION_MODE=STANDARD

// Medium-security apps (e-commerce, SaaS)
VALIDATION_MODE=ADVANCED

// High-security apps (banking, healthcare)
VALIDATION_MODE=STRICT

STANDARD Mode

  • ✅ Fast validation
  • ✅ Works with dynamic IPs
  • ⚠️ Less secure against session hijacking

ADVANCED Mode

  • ✅ Device binding
  • ✅ Better mobile security
  • ⚠️ Requires device ID management

STRICT Mode

  • ✅ Maximum security
  • ✅ Detects browser changes
  • ⚠️ May break with browser updates

Password Security

Enforce Strong Passwords

json
{
  "password_min_length": 12,
  "password_require_uppercase": true,
  "password_require_lowercase": true,
  "password_require_numbers": true,
  "password_require_special": true
}

Validate on Frontend

typescript
function validatePassword(password: string): string[] {
  const errors: string[] = [];
  
  if (password.length < 12) {
    errors.push('Password must be at least 12 characters');
  }
  if (!/[A-Z]/.test(password)) {
    errors.push('Password must contain uppercase letter');
  }
  if (!/[a-z]/.test(password)) {
    errors.push('Password must contain lowercase letter');
  }
  if (!/[0-9]/.test(password)) {
    errors.push('Password must contain number');
  }
  if (!/[!@#$%^&*]/.test(password)) {
    errors.push('Password must contain special character');
  }
  
  return errors;
}

Never Log Passwords

typescript
// ❌ NEVER do this!
console.log('User password:', password);
logger.info(`Login attempt with password: ${password}`);

// ✅ Log safely
logger.info(`Login attempt for user: ${email}`);

Rate Limiting

Respect Rate Limits

typescript
async function loginWithRetry(email: string, password: string) {
  const response = await fetch(`${FLOWLESS_URL}/auth/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ email, password }),
  });
  
  if (response.status === 429) {
    const resetTime = response.headers.get('X-RateLimit-Reset');
    const waitSeconds = parseInt(resetTime!) - Math.floor(Date.now() / 1000);
    
    throw new Error(`Rate limited. Try again in ${waitSeconds} seconds`);
  }
  
  return response.json();
}

Implement Client-Side Rate Limiting

typescript
class RateLimiter {
  private attempts: number[] = [];
  
  canAttempt(maxAttempts: number, windowMs: number): boolean {
    const now = Date.now();
    this.attempts = this.attempts.filter(time => now - time < windowMs);
    
    if (this.attempts.length >= maxAttempts) {
      return false;
    }
    
    this.attempts.push(now);
    return true;
  }
}

const loginLimiter = new RateLimiter();

async function login(email: string, password: string) {
  if (!loginLimiter.canAttempt(5, 15 * 60 * 1000)) {
    throw new Error('Too many login attempts. Please wait 15 minutes.');
  }
  
  // Proceed with login
}

Input Validation

Validate Email Addresses

typescript
function isValidEmail(email: string): boolean {
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return emailRegex.test(email);
}

// Always normalize to lowercase
const email = userInput.toLowerCase().trim();

Sanitize User Input

typescript
function sanitizeInput(input: string): string {
  return input
    .trim()
    .replace(/[<>]/g, '') // Remove HTML tags
    .substring(0, 255); // Limit length
}

Trust Token Caching

Cache Securely

typescript
import { LRUCache } from 'lru-cache';

const trustTokenCache = new LRUCache<string, string>({
  max: 10000,
  ttl: 1000 * 60 * 5, // 5 minutes max
});

// ✅ Cache trust tokens
trustTokenCache.set(sessionId, trustToken);

// ✅ Validate before use
const cachedToken = trustTokenCache.get(sessionId);
if (cachedToken && isTokenValid(cachedToken)) {
  return decodeToken(cachedToken);
}

Don't Cache Too Long

typescript
// ❌ Too long - security risk
ttl: 1000 * 60 * 60 // 1 hour

// ✅ Recommended
ttl: 1000 * 60 * 5 // 5 minutes

Error Handling

Don't Leak Information

typescript
// ❌ Bad - reveals if email exists
if (!user) {
  return { error: 'Email not found' };
}
if (!passwordMatch) {
  return { error: 'Incorrect password' };
}

// ✅ Good - generic message
if (!user || !passwordMatch) {
  return { error: 'Invalid email or password' };
}

Log Security Events

typescript
// Log failed login attempts
logger.warn('Failed login attempt', {
  email,
  ip: req.ip,
  timestamp: new Date(),
});

// Log successful logins
logger.info('Successful login', {
  userId: user.id,
  ip: req.ip,
  timestamp: new Date(),
});

Production Checklist

Before Going Live

  • [ ] Bridge Secret stored in environment variables
  • [ ] HTTPS enabled on all endpoints
  • [ ] Rate limiting configured
  • [ ] Password requirements enforced
  • [ ] Email verification enabled
  • [ ] Session duration appropriate
  • [ ] Validation mode selected
  • [ ] Error messages don't leak info
  • [ ] Logging configured
  • [ ] Monitoring enabled
  • [ ] Backup plan in place
  • [ ] Security audit completed

Incident Response

If Bridge Secret is Compromised

  1. Immediately rotate the secret in Pubflow dashboard
  2. Update environment variables in all environments
  3. Restart all backend services
  4. Invalidate all active sessions
  5. Notify users to re-login
  6. Investigate how the breach occurred
  7. Document the incident

Next Steps