Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Cloud Firestore: Enforcing Unique User Names

The Problem

I have seen this question several times (also in the context of the Firebase Real-Time Database), but I haven't seen a convincing answer to it. The problem statement is fairly simple:

How can (authenticated) users choose a username that hasn't been taken yet?

First of all, the why: After a user authenticates, they have a unique user ID. Many web-apps, however, let the user choose a "display name" (how the user wants to appear on the website), in order to protect the users personal data (like real name).

The Users Collection

Given a data structure like the following it is possible to store a username along with other data for each user:

/users  (collection)     /{uid}  (document)         - name: "<the username>"         - foo: "<other data>" 

However, nothing prevents another user (with a different {uid}) to store the same name in their record. As far as I know, there is no "security rule" that allows us to check if the name has already been by another user.

Note: A client side check is possible, but unsafe as a malicious client could omit the check.

The Reverse Mapping

Popular solutions are creating a collection with a reverse mapping:

/usernames  (collection)     /{name}  (document)        - uid: "<the auth {uid} field>" 

Given this reverse mapping, it is possible to write a security rule to enforce that a username is not already taken:

match /users/{userId} {   allow read: if true;   allow create, update: if       request.auth.uid == userId &&       request.resource.data.name is string &&       request.resource.data.name.size() >= 3 &&       get(/PATH/usernames/$(request.resource.data.name)).data.uid == userId; } 

and to force a user to create a usernames document first:

match /usernames/{name} {   allow read: if true;   allow create: if       request.resource.data.size() == 1 &&       request.resource.data.uid is string &&       request.resource.data.uid == request.auth.uid; } 

I believe the solution is half-way there. However, there are still a few unsolved issues.

Remaining Issues / Questions

This implementation is quite involved already but it doesn't even solve the problem of users that want to change their user name (requires record deletion or update rules, etc.)

Another issue is, nothing prevents a user from adding multiple records in the usernames collection, effectively snatching all good usernames to sabotage the system.

So to the questions:

  • Is there a simpler solution to enforce unique usernames?
  • How can spamming the usernames collection be prevented?
  • How can the username checks be made case-insensitive?

I tried also enforcing existence of the users, with another exists() rule for the /usernames collection and then committing a batch write operation, however, this doesn't seem to work ("Missing or insufficient permissions" error).

Another note: I have seen solutions with client-side checks. BUT THESE ARE UNSAFE. Any malicious client can modify the code, and omit checks.

like image 571
crazypeter Avatar asked Nov 21 '17 05:11

crazypeter


People also ask

How do I add unique data to firestore?

Yes, this is possible using a combination of two collections, Firestore rules and batched writes. The simple idea is, using a batched write, you write your document to your "data" collection and at the same write to a separate "index" collection where you index the value of the field that you want to be unique.

Are firebase IDS unique?

The keys generated by calling add() in Firestore are not tied to the collection on which you call add() . Instead they are random identifiers that are statistically guaranteed to be unique.

How do you add a custom ID to firestore?

To add Document with Custom ID to Firestore with JavaScript, we call the set method. db. collection("cities"). doc("LA").

How do I create a unique field in firebase?

If you need some value (or combination of values) to be unique, you need to create a node that contains that value (or combination) as its key. If you need to guarantee that multiple values (or combinations) are unique, you'll need multiple of such nodes.


2 Answers

@asciimike on twitter is a firebase security rules developer. He says there is currently no way to enforce uniqueness on a key on a document. https://twitter.com/asciimike/status/937032291511025664

Since firestore is based on Google Cloud datastore it inherits this issue. It's been a long standing request since 2008. https://issuetracker.google.com/issues/35875869#c14

However, you can achieve your goal by using firebase functions and some strict security rules.

You can view my entire proposed solution on medium. https://medium.com/@jqualls/firebase-firestore-unique-constraints-d0673b7a4952

like image 59
jqualls Avatar answered Oct 04 '22 06:10

jqualls


Created another, pretty simple solution for me.

I have usernames collection to storing unique values. username is available if the document doesn't exist, so it is easy to check on front-end.

Also, I added the pattern ^([a-z0-9_.]){5,30}$ to valide a key value.

Checking everything with Firestore rules:

function isValidUserName(username){   return username.matches('^([a-z0-9_.]){5,30}$'); }  function isUserNameAvailable(username){   return isValidUserName(username) && !exists(/databases/$(database)/documents/usernames/$(username)); }  match /users/{userID} {   allow update: if request.auth.uid == userID        && (request.resource.data.username == resource.data.username         || isUserNameAvailable(request.resource.data.username)       ); }  match /usernames/{username} {   allow get: if isValidUserName(username); } 

Firestore rules will not allow updating user's document in case if the username already exists or have an invalid value.

So, Cloud Functions will be handling only in case if the username has a valid value and doesn't exist yet. So, your server will have much less work.

Everything you need with cloud functions is to update usernames collection:

const functions = require("firebase-functions"); const admin = require("firebase-admin");  admin.initializeApp(functions.config().firebase);  exports.onUserUpdate = functions.firestore   .document("users/{userID}")   .onUpdate((change, context) => {     const { before, after } = change;     const { userID } = context.params;      const db = admin.firestore();      if (before.get("username") !== after.get('username')) {       const batch = db.batch()        // delete the old username document from the `usernames` collection       if (before.get('username')) {         // new users may not have a username value         batch.delete(db.collection('usernames')           .doc(before.get('username')));       }        // add a new username document       batch.set(db.collection('usernames')         .doc(after.get('username')), { userID });        return batch.commit();     }     return true;   }); 
like image 27
Bohdan Didukh Avatar answered Oct 04 '22 05:10

Bohdan Didukh