Skip to the content.

Security & Rules

This guide covers security best practices, Firestore security rules, and authentication patterns when using Firebase ORM.

Firestore Security Rules

Basic Security Rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Users can only access their own data
    match /users/{userId} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
    
    // Public read, authenticated write
    match /posts/{postId} {
      allow read: if true;
      allow write: if request.auth != null;
    }
    
    // Role-based access
    match /admin/{document=**} {
      allow read, write: if request.auth != null && 
        get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
    }
  }
}

Hierarchical Data Security

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Website members can only access their website's data
    match /websites/{websiteId}/members/{memberId} {
      allow read, write: if request.auth != null && 
        // Check if user is a member of this website
        exists(/databases/$(database)/documents/websites/$(websiteId)/members/$(request.auth.uid));
    }
    
    // Nested collections with inheritance
    match /websites/{websiteId}/{document=**} {
      allow read, write: if request.auth != null && 
        // Check website ownership or membership
        (get(/databases/$(database)/documents/websites/$(websiteId)).data.ownerId == request.auth.uid ||
         exists(/databases/$(database)/documents/websites/$(websiteId)/members/$(request.auth.uid)));
    }
  }
}

Field-Level Security

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /users/{userId} {
      allow read: if request.auth != null;
      allow update: if request.auth != null && 
        request.auth.uid == userId &&
        // Prevent users from changing sensitive fields
        !request.resource.data.diff(resource.data).affectedKeys().hasAny(['role', 'isVerified', 'credits']);
      
      allow create: if request.auth != null && 
        request.auth.uid == userId &&
        // Ensure required fields are set on creation
        request.resource.data.keys().hasAll(['name', 'email']) &&
        // Set default role
        request.resource.data.role == 'user';
    }
  }
}

Authentication Integration

User Model with Authentication

import { Field, BaseModel, Model } from "@arbel/firebase-orm";

@Model({
  reference_path: "users",
  path_id: "user_id"
})
export class User extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @Field({ is_required: true })
  public email!: string;

  @Field({ is_required: false })
  public role: string = 'user';

  @Field({ is_required: false, field_name: "is_verified" })
  public isVerified: boolean = false;

  @Field({ field_name: "created_at" })
  public createdAt?: string;

  @Field({ field_name: "last_login" })
  public lastLogin?: string;

  // Security methods
  hasRole(role: string): boolean {
    return this.role === role;
  }

  hasPermission(permission: string): boolean {
    const rolePermissions: Record<string, string[]> = {
      'admin': ['read', 'write', 'delete', 'manage_users'],
      'moderator': ['read', 'write', 'moderate'],
      'user': ['read', 'write_own']
    };

    return rolePermissions[this.role]?.includes(permission) || false;
  }

  async updateLastLogin(): Promise<void> {
    this.lastLogin = new Date().toISOString();
    await this.save();
  }
}

Authentication Service

import { getAuth, signInWithEmailAndPassword, createUserWithEmailAndPassword } from 'firebase/auth';
import { FirestoreOrmRepository } from '@arbel/firebase-orm';

export class AuthService {
  private auth = getAuth();

  async signUp(email: string, password: string, userData: any): Promise<User> {
    try {
      // Create Firebase Auth user
      const userCredential = await createUserWithEmailAndPassword(this.auth, email, password);
      
      // Create user model
      const user = new User();
      user.setId(userCredential.user.uid);
      user.email = email;
      user.name = userData.name;
      user.createdAt = new Date().toISOString();
      
      await user.save();
      return user;
    } catch (error) {
      console.error('Sign up failed:', error);
      throw error;
    }
  }

  async signIn(email: string, password: string): Promise<User> {
    try {
      const userCredential = await signInWithEmailAndPassword(this.auth, email, password);
      
      // Load user model
      const user = new User();
      await user.load(userCredential.user.uid);
      
      // Update last login
      await user.updateLastLogin();
      
      return user;
    } catch (error) {
      console.error('Sign in failed:', error);
      throw error;
    }
  }

  async getCurrentUser(): Promise<User | null> {
    const firebaseUser = this.auth.currentUser;
    if (!firebaseUser) return null;

    try {
      const user = new User();
      await user.load(firebaseUser.uid);
      return user;
    } catch (error) {
      console.error('Failed to load current user:', error);
      return null;
    }
  }

  async signOut(): Promise<void> {
    await this.auth.signOut();
  }
}

Role-Based Access Control

// Permission decorator
function RequirePermission(permission: string) {
  return function (target: any, propertyName: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;

    descriptor.value = async function (...args: any[]) {
      const currentUser = await getCurrentUser();
      
      if (!currentUser) {
        throw new Error('Authentication required');
      }

      if (!currentUser.hasPermission(permission)) {
        throw new Error(`Permission denied: ${permission} required`);
      }

      return originalMethod.apply(this, args);
    };

    return descriptor;
  };
}

// Usage in services
export class AdminService {
  @RequirePermission('manage_users')
  async deleteUser(userId: string): Promise<void> {
    const user = new User();
    await user.load(userId);
    await user.destroy();
  }

  @RequirePermission('read')
  async getUsers(): Promise<User[]> {
    return await User.getAll();
  }
}

Data Validation and Sanitization

Model-Level Validation

import { Field, BaseModel, Model } from "@arbel/firebase-orm";

@Model({
  reference_path: "posts",
  path_id: "post_id"
})
export class Post extends BaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true })
  public content!: string;

  @Field({ field_name: "author_id" })
  public authorId!: string;

  @Field({ is_required: false })
  public status: 'draft' | 'published' | 'archived' = 'draft';

  @Field({ field_name: "created_at" })
  public createdAt?: string;

  // Validation hooks
  async beforeSave(): Promise<void> {
    await this.validate();
    await this.sanitize();
    
    if (!this.createdAt) {
      this.createdAt = new Date().toISOString();
    }
  }

  private async validate(): Promise<void> {
    const errors: string[] = [];

    // Title validation
    if (!this.title || this.title.trim().length === 0) {
      errors.push('Title is required');
    }
    if (this.title && this.title.length > 200) {
      errors.push('Title must be less than 200 characters');
    }

    // Content validation
    if (!this.content || this.content.trim().length === 0) {
      errors.push('Content is required');
    }
    if (this.content && this.content.length > 10000) {
      errors.push('Content must be less than 10,000 characters');
    }

    // Author validation
    if (this.authorId) {
      const currentUser = await getCurrentUser();
      if (!currentUser || currentUser.getId() !== this.authorId) {
        errors.push('Invalid author');
      }
    }

    if (errors.length > 0) {
      throw new Error(`Validation failed: ${errors.join(', ')}`);
    }
  }

  private sanitize(): void {
    // Sanitize HTML content
    this.title = this.sanitizeHtml(this.title);
    this.content = this.sanitizeHtml(this.content);
  }

  private sanitizeHtml(html: string): string {
    // Basic HTML sanitization - use a proper library like DOMPurify in production
    return html
      .replace(/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/gi, '')
      .replace(/<iframe\b[^<]*(?:(?!<\/iframe>)<[^<]*)*<\/iframe>/gi, '')
      .replace(/on\w+="[^"]*"/gi, ''); // Remove event handlers
  }
}

Input Validation Service

export class ValidationService {
  static validateEmail(email: string): boolean {
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    return emailRegex.test(email);
  }

  static validatePassword(password: string): { valid: boolean; errors: string[] } {
    const errors: string[] = [];

    if (password.length < 8) {
      errors.push('Password must be at least 8 characters long');
    }
    if (!/[A-Z]/.test(password)) {
      errors.push('Password must contain at least one uppercase letter');
    }
    if (!/[a-z]/.test(password)) {
      errors.push('Password must contain at least one lowercase letter');
    }
    if (!/\d/.test(password)) {
      errors.push('Password must contain at least one number');
    }
    if (!/[!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?]/.test(password)) {
      errors.push('Password must contain at least one special character');
    }

    return { valid: errors.length === 0, errors };
  }

  static sanitizeInput(input: string): string {
    return input
      .trim()
      .replace(/[<>]/g, '') // Remove < and > characters
      .substring(0, 1000); // Limit length
  }

  static validateUrl(url: string): boolean {
    try {
      new URL(url);
      return true;
    } catch {
      return false;
    }
  }
}

Security Middleware

Request Validation Middleware

export class SecurityMiddleware {
  static async validateRequest(req: any, res: any, next: any) {
    try {
      // Check authentication
      const token = req.headers.authorization?.replace('Bearer ', '');
      if (!token) {
        return res.status(401).json({ error: 'Authentication required' });
      }

      // Verify token
      const decodedToken = await admin.auth().verifyIdToken(token);
      req.user = decodedToken;

      // Rate limiting
      await SecurityMiddleware.checkRateLimit(req.user.uid);

      // Input validation
      SecurityMiddleware.validateRequestData(req.body);

      next();
    } catch (error) {
      res.status(401).json({ error: 'Invalid authentication' });
    }
  }

  private static rateLimitStore = new Map<string, { count: number; resetTime: number }>();

  private static async checkRateLimit(userId: string): Promise<void> {
    const now = Date.now();
    const windowMs = 60 * 1000; // 1 minute
    const maxRequests = 100; // Max 100 requests per minute

    const userLimit = this.rateLimitStore.get(userId);
    
    if (!userLimit || now > userLimit.resetTime) {
      this.rateLimitStore.set(userId, { count: 1, resetTime: now + windowMs });
      return;
    }

    if (userLimit.count >= maxRequests) {
      throw new Error('Rate limit exceeded');
    }

    userLimit.count++;
  }

  private static validateRequestData(data: any): void {
    if (!data) return;

    // Check for common injection patterns
    const dangerousPatterns = [
      /<script/i,
      /javascript:/i,
      /on\w+=/i,
      /data:text\/html/i
    ];

    const checkValue = (value: any): void => {
      if (typeof value === 'string') {
        for (const pattern of dangerousPatterns) {
          if (pattern.test(value)) {
            throw new Error('Invalid input detected');
          }
        }
      } else if (typeof value === 'object' && value !== null) {
        Object.values(value).forEach(checkValue);
      }
    };

    checkValue(data);
  }
}

CORS and Security Headers

export class SecurityHeaders {
  static setSecurityHeaders(req: any, res: any, next: any) {
    // Content Security Policy
    res.setHeader('Content-Security-Policy', 
      "default-src 'self'; " +
      "script-src 'self' 'unsafe-inline' https://apis.google.com; " +
      "style-src 'self' 'unsafe-inline'; " +
      "img-src 'self' data: https:; " +
      "connect-src 'self' https://firestore.googleapis.com https://firebase.googleapis.com;"
    );

    // Prevent clickjacking
    res.setHeader('X-Frame-Options', 'DENY');

    // Prevent MIME type sniffing
    res.setHeader('X-Content-Type-Options', 'nosniff');

    // XSS Protection
    res.setHeader('X-XSS-Protection', '1; mode=block');

    // HSTS (HTTPS only)
    res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains');

    // Prevent referrer leakage
    res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');

    next();
  }
}

Secure Model Patterns

Audit Trail

@Model({
  reference_path: "audit_logs",
  path_id: "audit_id"
})
export class AuditLog extends BaseModel {
  @Field({ is_required: true, field_name: "user_id" })
  public userId!: string;

  @Field({ is_required: true })
  public action!: string;

  @Field({ is_required: true, field_name: "resource_type" })
  public resourceType!: string;

  @Field({ is_required: true, field_name: "resource_id" })
  public resourceId!: string;

  @Field({ is_required: false })
  public changes?: any;

  @Field({ field_name: "ip_address" })
  public ipAddress?: string;

  @Field({ field_name: "user_agent" })
  public userAgent?: string;

  @Field({ field_name: "created_at" })
  public createdAt!: string;

  static async log(
    userId: string,
    action: string,
    resourceType: string,
    resourceId: string,
    changes?: any,
    metadata?: any
  ): Promise<void> {
    const log = new AuditLog();
    log.userId = userId;
    log.action = action;
    log.resourceType = resourceType;
    log.resourceId = resourceId;
    log.changes = changes;
    log.ipAddress = metadata?.ipAddress;
    log.userAgent = metadata?.userAgent;
    log.createdAt = new Date().toISOString();

    await log.save();
  }
}

// Add audit logging to model operations
export class SecureBaseModel extends BaseModel {
  async save(): Promise<this> {
    const isNew = !this.getId();
    const currentUser = await getCurrentUser();
    
    if (currentUser) {
      await AuditLog.log(
        currentUser.getId(),
        isNew ? 'create' : 'update',
        this.constructor.name,
        this.getId() || 'new',
        this.toJSON()
      );
    }

    return super.save();
  }

  async destroy(): Promise<void> {
    const currentUser = await getCurrentUser();
    
    if (currentUser) {
      await AuditLog.log(
        currentUser.getId(),
        'delete',
        this.constructor.name,
        this.getId(),
        this.toJSON()
      );
    }

    return super.destroy();
  }
}

Soft Delete Pattern

@Model({
  reference_path: "posts",
  path_id: "post_id"
})
export class Post extends SecureBaseModel {
  @Field({ is_required: true })
  public title!: string;

  @Field({ is_required: true })
  public content!: string;

  @Field({ field_name: "deleted_at" })
  public deletedAt?: string;

  @Field({ field_name: "deleted_by" })
  public deletedBy?: string;

  // Override destroy to implement soft delete
  async destroy(): Promise<void> {
    const currentUser = await getCurrentUser();
    
    this.deletedAt = new Date().toISOString();
    this.deletedBy = currentUser?.getId();
    
    await this.save();
  }

  // Method to permanently delete
  async forceDestroy(): Promise<void> {
    await super.destroy();
  }

  // Query only non-deleted records
  static async getActive(): Promise<Post[]> {
    return await Post.query()
      .where('deletedAt', '==', null)
      .get();
  }

  // Restore soft-deleted record
  async restore(): Promise<void> {
    this.deletedAt = undefined;
    this.deletedBy = undefined;
    await this.save();
  }

  isDeleted(): boolean {
    return !!this.deletedAt;
  }
}

Data Encryption

Field-Level Encryption

import * as crypto from 'crypto';

export class EncryptionService {
  private static readonly algorithm = 'aes-256-gcm';
  private static readonly keyLength = 32;

  static encrypt(text: string, key: string): string {
    const iv = crypto.randomBytes(16);
    const keyBuffer = crypto.scryptSync(key, 'salt', this.keyLength);
    const cipher = crypto.createCipher(this.algorithm, keyBuffer);
    cipher.setAAD(Buffer.from('additional-data'));

    let encrypted = cipher.update(text, 'utf8', 'hex');
    encrypted += cipher.final('hex');

    const authTag = cipher.getAuthTag();

    return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
  }

  static decrypt(encryptedData: string, key: string): string {
    const [ivHex, authTagHex, encrypted] = encryptedData.split(':');
    
    const iv = Buffer.from(ivHex, 'hex');
    const authTag = Buffer.from(authTagHex, 'hex');
    const keyBuffer = crypto.scryptSync(key, 'salt', this.keyLength);
    
    const decipher = crypto.createDecipher(this.algorithm, keyBuffer);
    decipher.setAAD(Buffer.from('additional-data'));
    decipher.setAuthTag(authTag);

    let decrypted = decipher.update(encrypted, 'hex', 'utf8');
    decrypted += decipher.final('utf8');

    return decrypted;
  }
}

// Encrypted field decorator
function EncryptedField(options: any = {}) {
  return function (target: any, propertyKey: string) {
    const privateKey = `_${propertyKey}`;
    
    Object.defineProperty(target, propertyKey, {
      get() {
        const encryptedValue = this[privateKey];
        if (!encryptedValue) return undefined;
        
        const encryptionKey = this.getEncryptionKey();
        return EncryptionService.decrypt(encryptedValue, encryptionKey);
      },
      
      set(value: string) {
        if (value) {
          const encryptionKey = this.getEncryptionKey();
          this[privateKey] = EncryptionService.encrypt(value, encryptionKey);
        } else {
          this[privateKey] = value;
        }
      }
    });

    // Register the private field for serialization
    Field(options)(target, privateKey);
  };
}

// Usage
@Model({
  reference_path: "sensitive_data",
  path_id: "data_id"
})
export class SensitiveData extends BaseModel {
  @Field({ is_required: true })
  public name!: string;

  @EncryptedField({ field_name: "encrypted_ssn" })
  public ssn!: string;

  @EncryptedField({ field_name: "encrypted_credit_card" })
  public creditCard!: string;

  private getEncryptionKey(): string {
    // In production, use proper key management
    return process.env.ENCRYPTION_KEY || 'default-key';
  }
}

Security Best Practices

1. Authentication & Authorization

2. Data Validation

3. Firestore Security Rules

4. Network Security

5. Data Protection

6. Error Handling

7. Development Practices