Skip to main content
Follow these best practices to build secure, performant, and user-friendly authentication systems with ValidAuth.

Security Best Practices

Always Validate on Server Side

Never rely solely on client-side validation. Always validate authentication data on the server to prevent malicious users from bypassing client-side checks.
// ✅ Good: Validate on both client and server

// Client-side (React)
function LoginForm() {
  const handleSubmit = async (formData) => {
    // Client-side validation for UX
    const clientValidation = validateLogin(formData);
    if (!clientValidation.valid) {
      showErrors(clientValidation.errors);
      return;
    }
    
    // Send to server
    const response = await fetch('/api/login', {
      method: 'POST',
      body: JSON.stringify(formData)
    });
  };
}

// Server-side (Express)
app.post('/api/login', (req, res) => {
  // Server-side validation for security
  const serverValidation = validateLogin(req.body);
  if (!serverValidation.valid) {
    return res.status(400).json({ errors: serverValidation.errors });
  }
  
  // Proceed with authentication
  authenticateUser(req.body);
});

Use Strong Password Requirements

Enforce strict password policies to protect user accounts.
import { isPassword } from 'validauth';

// ✅ Good: Strong password requirements
const passwordValidation = isPassword(password, {
  minLength: 12,              // Longer passwords are more secure
  maxLength: 128,             // Prevent denial-of-service attacks
  requireUppercase: true,
  requireLowercase: true,
  requireNumbers: true,
  requireSymbols: true,
  forbidCommonPasswords: true, // Critical for security
  details: true
});

// ❌ Bad: Weak requirements
const weakValidation = isPassword(password, {
  minLength: 6,
  requireSymbols: false,
  forbidCommonPasswords: false // Leaves users vulnerable
});
The forbidCommonPasswords option checks against 1,000+ common passwords from breach databases. Always enable this in production.

Block Temporary Email Services

Prevent spam and fake accounts by blocking disposable email domains.
import { isEmail } from 'validauth';

// ✅ Good: Block temporary email providers
const blockedDomains = [
  'tempmail.com',
  'throwaway.email',
  '10minutemail.com',
  'guerrillamail.com',
  'mailinator.com',
  'trashmail.com'
];

const emailValidation = isEmail(email, {
  blockedDomains,
  requireTLD: true,
  details: true
});

Protect Against Username-Password Correlation

Prevent users from creating passwords that contain their username or email.
import { isPassword } from 'validauth';

function validatePassword(password, username, email) {
  // Standard validation
  const result = isPassword(password, {
    minLength: 12,
    requireSymbols: true,
    forbidCommonPasswords: true,
    details: true
  });
  
  if (!result.valid) {
    return result;
  }
  
  // Additional security checks
  const passwordLower = password.toLowerCase();
  const usernameLower = username.toLowerCase();
  const emailLocal = email.split('@')[0].toLowerCase();
  
  if (passwordLower.includes(usernameLower)) {
    return {
      valid: false,
      errors: ['Password cannot contain your username']
    };
  }
  
  if (passwordLower.includes(emailLocal)) {
    return {
      valid: false,
      errors: ['Password cannot contain your email address']
    };
  }
  
  return { valid: true };
}

Implement Rate Limiting

Protect against brute-force attacks by limiting validation attempts.
import { validateOTP } from 'validauth';

// Rate limiting example for OTP verification
class RateLimiter {
  constructor() {
    this.attempts = new Map();
  }
  
  checkRateLimit(identifier, maxAttempts = 5, windowMs = 15 * 60 * 1000) {
    const now = Date.now();
    const userAttempts = this.attempts.get(identifier) || [];
    
    // Remove old attempts outside the time window
    const recentAttempts = userAttempts.filter(time => now - time < windowMs);
    
    if (recentAttempts.length >= maxAttempts) {
      return {
        allowed: false,
        retryAfter: windowMs - (now - recentAttempts[0])
      };
    }
    
    // Record this attempt
    recentAttempts.push(now);
    this.attempts.set(identifier, recentAttempts);
    
    return { allowed: true };
  }
}

const limiter = new RateLimiter();

function handleLogin(email, password) {
  const rateCheck = limiter.checkRateLimit(email);
  
  if (!rateCheck.allowed) {
    return {
      success: false,
      error: 'Too many attempts. Please try again later.',
      retryAfter: rateCheck.retryAfter
    };
  }
  
  // Proceed with validation
  // ...
}

Performance Optimization

Use Simple Validation for Login

Login forms should use minimal validation rules for better performance and UX. Save strict validation for registration and password changes.
import { isEmail, isPassword } from 'validauth';

// ✅ Good: Lightweight validation for login
function validateLoginCredentials(email, password) {
  const emailValid = isEmail(email, {
    requireTLD: true,
    details: false // Simple boolean response
  });
  
  const passwordValid = password.length > 0;
  
  return emailValid && passwordValid;
}

// ❌ Bad: Excessive validation for login
function overValidateLogin(email, password) {
  const emailValid = isEmail(email, {
    allowPlusAddressing: false,
    blockedDomains: [...], // Unnecessary for login
    details: true
  });
  
  const passwordValid = isPassword(password, {
    minLength: 12,
    requireSymbols: true,
    forbidCommonPasswords: true // Too slow for login
  });
}

Cache Validation Results

Avoid redundant validation by caching results for unchanged input.
class CachedValidator {
  constructor() {
    this.cache = new Map();
  }
  
  validateEmail(email, options) {
    const cacheKey = `${email}-${JSON.stringify(options)}`;
    
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey);
    }
    
    const result = isEmail(email, options);
    this.cache.set(cacheKey, result);
    
    // Limit cache size
    if (this.cache.size > 1000) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }
    
    return result;
  }
}

Debounce Real-Time Validation

Reduce validation frequency for better performance in real-time scenarios.
import { isPassword, getPasswordStrength } from 'validauth';

// Debounce helper
function debounce(func, wait) {
  let timeout;
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout);
      func(...args);
    };
    clearTimeout(timeout);
    timeout = setTimeout(later, wait);
  };
}

// ✅ Good: Debounced real-time validation
const debouncedValidation = debounce((password, callback) => {
  const strength = getPasswordStrength(password, { details: true });
  const validation = isPassword(password, {
    minLength: 10,
    requireSymbols: true,
    details: true
  });
  
  callback({ strength, validation });
}, 300); // Wait 300ms after user stops typing

// Usage in React
function PasswordInput() {
  const [feedback, setFeedback] = useState(null);
  
  const handleChange = (e) => {
    const password = e.target.value;
    debouncedValidation(password, setFeedback);
  };
  
  return <input type="password" onChange={handleChange} />;
}

Tree-Shake Unused Validators

Import only the validators you need to reduce bundle size.
// ✅ Good: Import only what you need
import { isEmail, isPassword } from 'validauth';

// ❌ Bad: Import everything
import * as validauth from 'validauth';
  • isEmail: ~4.4KB
  • isPassword: ~6KB (includes common passwords list)
  • isUsername: ~3.6KB
  • getPasswordStrength: ~5KB
  • Full library: ~14KB minified, ~5KB gzipped

Error Handling Best Practices

Provide Specific Error Messages

Use the details: true option to give users actionable feedback.
import { isPassword } from 'validauth';

// ✅ Good: Detailed errors help users fix issues
const result = isPassword(password, {
  minLength: 12,
  requireSymbols: true,
  details: true
});

if (!result.valid) {
  // Display specific errors
  result.errors.forEach(error => {
    showError(error); // "Password must be at least 12 characters long"
  });
}

// ❌ Bad: Generic error message
if (!isPassword(password)) {
  showError('Invalid password'); // Not helpful
}

Handle Validation Errors Gracefully

Provide a good user experience even when validation fails.
function displayValidationErrors(errors) {
  // Group errors by field
  const errorsByField = {};
  
  Object.keys(errors).forEach(field => {
    errorsByField[field] = Array.isArray(errors[field]) 
      ? errors[field] 
      : [errors[field]];
  });
  
  // Display inline errors
  Object.keys(errorsByField).forEach(field => {
    const inputElement = document.getElementById(field);
    const errorElement = document.getElementById(`${field}-error`);
    
    if (inputElement && errorElement) {
      inputElement.classList.add('error');
      errorElement.textContent = errorsByField[field][0];
      errorElement.style.display = 'block';
    }
  });
}

Don’t Leak Information in Error Messages

For login validation, avoid revealing whether the email exists. Use generic error messages to prevent user enumeration attacks.
// ✅ Good: Generic error message for login
function authenticateUser(email, password) {
  const user = findUserByEmail(email);
  
  if (!user || !verifyPassword(user, password)) {
    // Don't reveal if email exists
    return {
      success: false,
      error: 'Invalid email or password'
    };
  }
  
  return { success: true, user };
}

// ❌ Bad: Reveals which field is wrong
function badAuthentication(email, password) {
  const user = findUserByEmail(email);
  
  if (!user) {
    return { error: 'Email not found' }; // User enumeration risk
  }
  
  if (!verifyPassword(user, password)) {
    return { error: 'Incorrect password' };
  }
}

User Experience Guidelines

Show Password Strength in Real-Time

Help users create strong passwords with live feedback.
import { getPasswordStrength } from 'validauth';

function updatePasswordStrengthUI(password) {
  const strength = getPasswordStrength(password, { details: true });
  
  // Update strength bar
  const strengthBar = document.getElementById('strength-bar');
  strengthBar.style.width = `${strength.score}%`;
  strengthBar.className = `strength-bar ${strength.strength}`;
  
  // Update text feedback
  const strengthText = document.getElementById('strength-text');
  strengthText.textContent = `Strength: ${strength.strength.toUpperCase()}`;
  
  // Show crack time estimate
  const crackTime = document.getElementById('crack-time');
  crackTime.textContent = `Would take ${strength.crackTimeDisplay} to crack`;
}

Progressive Enhancement for Validation

Validate progressively as users complete fields.
// Validate on blur (when user leaves field)
function setupProgressiveValidation() {
  const emailInput = document.getElementById('email');
  const passwordInput = document.getElementById('password');
  const usernameInput = document.getElementById('username');
  
  emailInput.addEventListener('blur', () => {
    const result = isEmail(emailInput.value, { details: true });
    if (!result.valid) {
      showFieldError('email', result.errors[0]);
    } else {
      clearFieldError('email');
      showFieldSuccess('email');
    }
  });
  
  passwordInput.addEventListener('input', debounce(() => {
    const result = isPassword(passwordInput.value, {
      minLength: 10,
      details: true
    });
    
    if (passwordInput.value.length > 0) {
      updatePasswordStrength(passwordInput.value);
    }
  }, 300));
  
  usernameInput.addEventListener('blur', async () => {
    const result = isUsername(usernameInput.value, { details: true });
    if (!result.valid) {
      showFieldError('username', result.errors[0]);
    } else {
      // Check availability with server
      const available = await checkUsernameAvailability(usernameInput.value);
      if (available) {
        showFieldSuccess('username');
      } else {
        showFieldError('username', 'Username already taken');
      }
    }
  });
}

Provide Clear Password Requirements

Display requirements upfront so users know what’s expected.
function showPasswordRequirements() {
  return `
    <div class="password-requirements">
      <p>Password must:</p>
      <ul>
        <li>Be at least 12 characters long</li>
        <li>Contain at least one uppercase letter (A-Z)</li>
        <li>Contain at least one lowercase letter (a-z)</li>
        <li>Contain at least one number (0-9)</li>
        <li>Contain at least one special character (!@#$%^&*)</li>
        <li>Not be a commonly used password</li>
      </ul>
    </div>
  `;
}

Handle Edge Cases Gracefully

While ValidAuth defaults to disallowing special characters in usernames, you can enable them:
const result = isUsername(username, {
  allowSpecialChars: true,
  minLength: 3,
  maxLength: 30
});
However, consider the implications:
  • Special characters can complicate URL routing (e.g., /user/@john#doe)
  • They may be used for phishing (e.g., admin@site)
  • They can cause database or API issues
If you allow them, sanitize and escape properly in all contexts.
Plus addressing (e.g., [email protected]) has pros and cons:Allow it for:
  • User convenience (email filtering, tracking)
  • Legitimate use cases (newsletter management)
Block it for:
  • Preventing multiple accounts from same email
  • Reducing spam account creation
  • Stricter account verification
// For registration: block plus addressing
isEmail(email, { allowPlusAddressing: false });

// For newsletter signup: allow it
isEmail(email, { allowPlusAddressing: true });
Use different validation levels:
// Registration: strict validation
function validateRegistrationPassword(password) {
  return isPassword(password, {
    minLength: 12,
    requireSymbols: true,
    forbidCommonPasswords: true,
    details: true
  });
}

// Login: minimal validation
function validateLoginPassword(password) {
  // Just check it's not empty
  return password.length > 0;
}
Never validate password strength during login - it degrades performance and provides no security benefit.

Common Pitfalls to Avoid

Don’t Store Validation Results in Passwords

// ❌ Bad: Never store validation metadata with passwords
const user = {
  password: hashPassword(password),
  passwordStrength: getPasswordStrength(password), // Don't do this
};

// ✅ Good: Only store the hashed password
const user = {
  password: hashPassword(password)
};

Don’t Skip Validation for “Trusted” Sources

// ❌ Bad: Skipping validation for admin users
if (user.role !== 'admin') {
  validatePassword(password);
}

// ✅ Good: Validate all input regardless of source
const validation = validatePassword(password);
if (!validation.valid) {
  throw new Error('Invalid password');
}

Don’t Use Only Client-Side Validation

// ❌ Bad: Only validating on frontend
function handleSignup(formData) {
  if (validateForm(formData)) {
    // Send directly to database without server validation
    saveUser(formData);
  }
}

// ✅ Good: Validate on both client and server
function handleSignup(formData) {
  // Client validation
  if (!validateFormClientSide(formData)) {
    showErrors();
    return;
  }
  
  // Send to server for server-side validation
  fetch('/api/signup', {
    method: 'POST',
    body: JSON.stringify(formData)
  });
}

// Server endpoint
app.post('/api/signup', (req, res) => {
  // Server validation (critical!)
  if (!validateFormServerSide(req.body)) {
    return res.status(400).json({ error: 'Invalid data' });
  }
  saveUser(req.body);
});

Configuration Recommendations

Production Settings

Recommended ValidAuth configuration for production environments:
const productionConfig = {
  email: {
    allowPlusAddressing: false,
    requireTLD: true,
    blockedDomains: [
      'tempmail.com',
      'throwaway.email',
      '10minutemail.com',
      'guerrillamail.com',
      'mailinator.com'
    ],
    details: true
  },
  
  password: {
    minLength: 12,
    maxLength: 128,
    requireUppercase: true,
    requireLowercase: true,
    requireNumbers: true,
    requireSymbols: true,
    forbidCommonPasswords: true,
    details: true
  },
  
  username: {
    minLength: 4,
    maxLength: 20,
    allowSpecialChars: false,
    forbidSpaces: true,
    forbidStartingNumber: true,
    blockedUsernames: [
      'admin', 'administrator', 'root', 'system',
      'moderator', 'mod', 'support', 'help',
      'official', 'staff', 'owner'
    ],
    details: true
  }
};

Development Settings

More lenient configuration for development and testing:
const developmentConfig = {
  email: {
    allowPlusAddressing: true,
    requireTLD: false, // Allow localhost emails
    blockedDomains: [],
    details: true
  },
  
  password: {
    minLength: 6,
    maxLength: 128,
    requireUppercase: false,
    requireLowercase: false,
    requireNumbers: false,
    requireSymbols: false,
    forbidCommonPasswords: false, // Allow weak passwords for testing
    details: true
  },
  
  username: {
    minLength: 2,
    maxLength: 50,
    allowSpecialChars: true,
    forbidSpaces: false,
    forbidStartingNumber: false,
    blockedUsernames: [],
    details: true
  }
};
Use environment variables to switch between production and development configurations:
const config = process.env.NODE_ENV === 'production' 
  ? productionConfig 
  : developmentConfig;

Summary

1

Security First

  • Always validate on server side
  • Use strong password requirements with forbidCommonPasswords: true
  • Block temporary email domains
  • Implement rate limiting
  • Don’t leak information in error messages
2

Optimize Performance

  • Use simple validation for login forms
  • Cache validation results when appropriate
  • Debounce real-time validation
  • Import only the validators you need
3

Enhance User Experience

  • Show password strength in real-time
  • Provide specific, actionable error messages
  • Display requirements upfront
  • Use progressive validation
  • Handle edge cases gracefully
4

Follow Best Practices

  • Use different validation rules for registration vs login
  • Validate all input regardless of source
  • Store only hashed passwords, never validation metadata
  • Use environment-specific configurations

Build docs developers (and LLMs) love