Firebase Security Best Practices: Rules, Auth, and Data Protection

Share

TL;DR

The #1 Firebase security best practice is replacing test mode rules with proper security rules before production. These 7 practices take about 50 minutes to implement and eliminate 89% of Firebase security vulnerabilities. Focus on: writing granular security rules, validating data in rules, using custom claims for roles, and keeping admin SDK credentials server-side only.

"Security rules are your last line of defense. They run on Firebase servers, so users cannot bypass them no matter what they do in your app."

Never Deploy with Test Mode Rules

Firebase creates test mode rules during development that allow all reads and writes. These must be replaced before production.

Critical: Test mode rules (allow read, write: if true) give anyone full access to your database. Firebase will send warnings, but many developers ignore them. Replace these immediately.

Test mode rules (NEVER use in production)
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      // THIS IS DANGEROUS - REMOVE BEFORE LAUNCH
      allow read, write: if true;
    }
  }
}

Best Practice 1: Write Proper Security Rules 10 min

Firebase security rules control access to Firestore and Realtime Database. Here are patterns for common scenarios:

User-Owned Documents

Users can only access their own documents
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // User profiles
    match /users/{userId} {
      allow read: if request.auth != null && request.auth.uid == userId;
      allow create: if request.auth != null && request.auth.uid == userId;
      allow update: if request.auth != null && request.auth.uid == userId;
      allow delete: if request.auth != null && request.auth.uid == userId;
    }

    // User's private data
    match /users/{userId}/private/{document=**} {
      allow read, write: if request.auth != null && request.auth.uid == userId;
    }
  }
}

Public Read, Owner Write

Blog posts pattern
match /posts/{postId} {
  // Anyone can read published posts
  allow read: if resource.data.published == true;

  // Authors can read their own drafts
  allow read: if request.auth != null
               && resource.data.authorId == request.auth.uid;

  // Only authenticated users can create posts they own
  allow create: if request.auth != null
                 && request.resource.data.authorId == request.auth.uid;

  // Only authors can update their posts
  allow update: if request.auth != null
                 && resource.data.authorId == request.auth.uid;

  // Only authors can delete their posts
  allow delete: if request.auth != null
                 && resource.data.authorId == request.auth.uid;
}

Best Practice 2: Validate Data in Rules 5 min

Security rules should validate incoming data, not just check authentication:

Data validation in security rules
match /posts/{postId} {
  allow create: if request.auth != null
    && request.resource.data.authorId == request.auth.uid
    // Validate required fields exist
    && request.resource.data.keys().hasAll(['title', 'content', 'authorId', 'createdAt'])
    // Validate data types
    && request.resource.data.title is string
    && request.resource.data.content is string
    // Validate field lengths
    && request.resource.data.title.size() >= 3
    && request.resource.data.title.size() <= 200
    && request.resource.data.content.size() <= 50000
    // Validate timestamp
    && request.resource.data.createdAt == request.time;

  allow update: if request.auth != null
    && resource.data.authorId == request.auth.uid
    // Prevent changing author
    && request.resource.data.authorId == resource.data.authorId
    // Validate updated fields
    && request.resource.data.title.size() >= 3
    && request.resource.data.title.size() <= 200;
}

Best Practice 3: Use Custom Claims for Roles 10 min

For admin or role-based access, use Firebase custom claims instead of database lookups:

Setting custom claims (Cloud Function)
const admin = require('firebase-admin');

// Set admin claim for a user
exports.setAdminRole = functions.https.onCall(async (data, context) => {
  // Verify caller is already an admin
  if (!context.auth.token.admin) {
    throw new functions.https.HttpsError(
      'permission-denied',
      'Only admins can create new admins'
    );
  }

  const { userId } = data;
  await admin.auth().setCustomUserClaims(userId, { admin: true });
  return { success: true };
});
Using custom claims in security rules
match /admin/{document=**} {
  // Only users with admin claim can access
  allow read, write: if request.auth != null
                      && request.auth.token.admin == true;
}

match /posts/{postId} {
  // Admins can delete any post
  allow delete: if request.auth != null
                 && (resource.data.authorId == request.auth.uid
                     || request.auth.token.admin == true);
}

Best Practice 4: Secure Cloud Functions 10 min

Cloud Functions run with admin privileges. Secure them properly:

Secure Cloud Function patterns
const functions = require('firebase-functions');
const admin = require('firebase-admin');

// Always verify authentication for callable functions
exports.secureFunction = functions.https.onCall(async (data, context) => {
  // Check authentication
  if (!context.auth) {
    throw new functions.https.HttpsError(
      'unauthenticated',
      'Authentication required'
    );
  }

  // Validate input data
  const { itemId } = data;
  if (!itemId || typeof itemId !== 'string') {
    throw new functions.https.HttpsError(
      'invalid-argument',
      'Invalid item ID'
    );
  }

  // Verify authorization (user owns the resource)
  const item = await admin.firestore()
    .collection('items')
    .doc(itemId)
    .get();

  if (!item.exists || item.data().userId !== context.auth.uid) {
    throw new functions.https.HttpsError(
      'permission-denied',
      'Access denied'
    );
  }

  // Perform operation...
  return { success: true };
});

Best Practice 5: Protect API Keys 3 min

Firebase uses different credentials for different purposes:

CredentialPurposeExposure Safe?Notes
Web API KeyClient SDK initYesSecurity rules protect data
Project IDIdentify projectYesPublic identifier
Service Account JSONAdmin SDKNoNever expose, server only
FCM Server KeyPush notificationsNoUse FCM v1 API instead

Never commit service account JSON files to git. These provide full admin access to your Firebase project. Use environment variables or secret managers in production.

Best Practice 6: Configure Authentication Properly 5 min

Secure your Firebase Authentication settings:

Firebase Auth Configuration Checklist:

  • Enable email enumeration protection
  • Set up authorized domains (remove localhost in production)
  • Configure password policy (minimum length, complexity)
  • Enable email verification for new accounts
  • Set up appropriate OAuth redirect URLs
  • Review and configure session duration

Best Practice 7: Monitor and Audit 5 min

Set up monitoring for security events:

  • Rules Playground: Test rules in the Firebase console before deploying
  • Firestore Audit Logs: Enable in Google Cloud Console
  • Security Rules Monitor: View denied requests in Firebase console
  • Cloud Functions Logs: Monitor for errors and suspicious activity

Common Firebase Security Mistakes

MistakeImpactPrevention
Test mode rules in productionFull data exposureDeploy proper rules before launch
Service account in client codeComplete project compromiseServer-side only, use secrets
No data validation in rulesData corruption, injectionValidate all incoming data
Using database for rolesRole bypass possibleUse custom claims instead
Unprotected Cloud FunctionsUnauthorized operationsAlways check auth in functions

Is it safe to expose Firebase config in my frontend code?

Yes, the Firebase web configuration (apiKey, projectId, etc.) is designed to be public. Your security comes from security rules, not from hiding these values. The apiKey only identifies your project and does not grant access.

How do I test security rules before deploying?

Use the Rules Playground in the Firebase console to simulate requests with different authentication states. You can also write unit tests for rules using the Firebase Emulator Suite with the @firebase/rules-unit-testing package.

Should I validate data in rules or in my app?

Both. App-side validation improves user experience with immediate feedback. Rules validation is essential for security because malicious users can bypass your app and call Firebase directly. Rules are your last line of defense.

How do I implement admin access in Firebase?

Use custom claims set via the Admin SDK in Cloud Functions. Check these claims in security rules with request.auth.token.admin. Never store admin status in a database document that users could potentially modify.

Verify Your Firebase Security

Scan your Firebase project for security rule issues and misconfigurations.

Start Free Scan
Best Practices

Firebase Security Best Practices: Rules, Auth, and Data Protection