loading...

Firebase Firestore Rules with Custom Claims - an easy way

alvardev profile image Alvaro David Updated on ・3 min read

Firebase Firestore rules are a great tool to provide access control and data validation in a simple and expressive format, it has a great documentation and videos.

Custom Claims provides the ability to implement various access control strategies, including role-based access control, in Firebase apps. These custom attributes can give users different levels of access (roles), which are enforced in an application's security rules.

Both together are awesome but I found that the implementation could be a little bit confusing if you are not familiar with Firebase Authentication, so in this post we are going to create a strategy where we can take advantage of both of them.

For this post I will simplify as much as possible the structure of the Collections and Documents.

The Case

We have a Collection of articles with a title and a content.

Articles Collection

And we want that:

  • Editors can update those articles
  • Viewers can read those articles

So we create a Collection of access (roles).

Access collection
As you can see we reuse the Firebase Authentication User UID as a document id.

Without Custom Claims

If you are not using Custom Claims, your Rules will look something like this:

Rules without custom claims

There are two functions that read the Collection of access in order to know if the user (UID) can read/update the article.

This works, but there are two reads, one for the request itself and one more to check the access level, so we are duplicating the number of reads and if an API wants to use this logic the code will have to read that collection too.

We can improve that.

With Custom Claims

Ok, first at all: What is a claim?

Basically is the information that Firebase Authentication will return each time you validate a token.

For example:

{
    "aud": "my-project",
    "auth_time": 1234567890,
    "email": "alvardev@example.com",
    "email_verified": true,
    "exp": 1234567980,
    "firebase": {
        "identities": {
            "email": [
                "alvardev@example.com"
            ],
            "google.com": [
                "1234567890123456789012"
            ]
        },
        "sign_in_provider": "google.com"
    },
    "iat": 1234567890,
    "iss": "https://securetoken.google.com/my-project",
    "name": "Alvaro David",
    "picture": "https://urlformypicture.com/image",
    "sub": "123abc123abc123abc123",
    "user_id": "xIR9cPwbHBQb"
}

This information can be read by the Firestore Rules.

The strategy is to add some fields to this information and avoid to do a second read on our collections.

Custom claims are only used to provide access control. They are not designed to store additional data (such as profile and other custom data). Please read the docs.

In this case, we will add the 'level' field:

{"level": "editor"}  // or viewer

So our rules will be like:

Alt Text

Now it looks cleaner.

Sounds good... but how to implement that?

In both cases (with and without claims) the start point is the Collection of access, every time we update a document in this Collection we have to set the Custom Claim.

The perfect resource for this task is Firebase Functions! Every time a document is update we can trigger a functions that can set the custom claim based on the content updated.

index.js

const functions = require('firebase-functions');
const admin = require('firebase-admin');
admin.initializeApp();

exports.updateAccess = functions.firestore
  .document('access/{accessId}')
  .onUpdate((change, context) => {

    const newValue = change.after.data();
    const customClaims = {
      level: newValue.level
    };

    // Set custom user claims on this update.
    return. admin.auth().setCustomUserClaims(
               context.params.accessId, customClaims)
    .then(() => {
      console.log("Done!")
    })
    .catch(error => {
      console.log(error);
    });

});

deploy

firebase deploy --only functions

Now our claim looks like:

{
    "aud": "my-project",
    "auth_time": 1234567890,
    "email": "alvardev@example.com",
    "email_verified": true,
    "exp": 1234567980,
    "firebase": {
        "identities": {
            "email": [
                "alvardev@example.com"
            ],
            "google.com": [
                "1234567890123456789012"
            ]
        },
        "sign_in_provider": "google.com"
    },
    "iat": 1234567890,
    "iss": "https://securetoken.google.com/my-project",
    "name": "Alvaro David",
    "picture": "https://urlformypicture.com/image",
    "sub": "123abc123abc123abc123",
    "user_id": "xIR9cPwbHBQb",
    "level": "editor"  // There you are!
}

That's it! with these few steps we implemented a Custom Claim to enforce our Firestore Rules, those claims are easy to update due to the Firebase Functions triggers.

Hope it helps and my the source be with you :)

Posted on by:

alvardev profile

Alvaro David

@alvardev

GCP Cloud Architect and Software Engineer

Discussion

pic
Editor guide