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_abc123xyz789typescript
// Backend only
const bridgeSecret = process.env.BRIDGE_SECRET;❌ DON'T
typescript
// NEVER do this!
const bridgeSecret = 'bridge_secret_abc123xyz789'; // Hardcodedjavascript
// 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=STRICTSTANDARD 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 minutesError 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
- Immediately rotate the secret in Pubflow dashboard
- Update environment variables in all environments
- Restart all backend services
- Invalidate all active sessions
- Notify users to re-login
- Investigate how the breach occurred
- Document the incident
Next Steps
- Authentication Security - Auth-specific security
- Session Security - Session management security
- Rate Limiting - Configure rate limits
- Monitoring - Monitor security events