How to protect email addresses but make them searchable with Firestore

Viewed 640

I couldn't find anything on the topic, so here goes.

I'm creating an app with a Firebase Cloud Firestore database with users.

My goal is to "prevent people from stealing all email addresses but still make them searchable"

My user data is saved per user like so:

in /users/{userId}

{
  email: 'user@gmail.com',
  displayName: 'James Liverstone'
}

I can protect the user data with these rules:

match /users/{userId} {
  allow write, read: if request.auth.uid == userId
                     && request.auth.uid != null;
}

But what if I want to make it so someone can search for a friend in my app, by email or display name?

eg.

const searchVal = 'user@gmail.com' // search value from <input>

firebase.firestore().collection('users').where('email', '==', searchVal)

This is not possible because of the read rule. However, if I open up read to allow everyone, you could steal all email addresses of my users like so:

firebase.firestore().collection('users').get()

how can I prevent people from stealing all email addresses but still make them searchable?

So in short:

  • allow: firebase.firestore().collection('users').where('email', '==', searchVal)
  • prevent: firebase.firestore().collection('users').get()
2 Answers

It seems you can't enforce this with security rules, so your best best would be to write a Cloud Function (http or callable) that will perform the query safely and return the desired result to the client. This function would take the email address as an input argument and minimally output some boolean that indicates if the user exists.

There is a workaround without using Cloud Functions

One workaround for this using only firestore is to create an additional collection like so:

Every time a user is created, set an empty document with email address as the key:

const email = 'user@gmail.com' //get the email of the new user

firestore().doc(`searchUsers/${email}`).set({})

This way we have a collection called searchUsers with a bunch of empty documents with the email address as key.

Required security rules:

  • Prevent users from getting all these emails
    • with .collection('searchUsers').get()
  • Allow checking the existence for a single email address
    • with .doc('searchUsers/user@gmail.com').get()

Set the security rules like so:

match /searchUsers/{value} {
  allow create: if request.auth != null
                && value == request.auth.token.email;
  allow list: if false;
  allow get;
}

These security rules explained:

  • allow create rule: "only allow users to create a doc with their own email address"
  • allow list rule: "Prevent users from getting all these emails"
    • with .collection('searchUsers').get()
  • allow get rule: "you can query for a single doc with the email as key to check existence"
    • with .doc('searchUsers/user@gmail.com').get()

In practice

You will have a search form <input> and target this to execute:

const searchVal = 'user@gmail.com' // search value from <input>

const docRef = await firestore().doc(`searchUsers/${searchVal}`).get()
const userExists = docRef.exists
Related