DEV Community

Alvaro David
Alvaro David

Posted on • Edited on

Firebase Firestore Rules with Custom Claims - an easy way

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"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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);
    });

});
Enter fullscreen mode Exit fullscreen mode

deploy

firebase deploy --only functions
Enter fullscreen mode Exit fullscreen mode

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!
}
Enter fullscreen mode Exit fullscreen mode

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 :)

Top comments (4)

Collapse
 
pradeephere profile image
pradeep-here • Edited

Hi, is the custom-claims still usable in Firestore Rules ?
It worked for me few months back, but same rule is not working now.

Getting error - Missing or insufficient permissions

Rule used:

    allow read: if request.auth.token.level == 4
Enter fullscreen mode Exit fullscreen mode

I have verified the user has the custom token claim. Printing tokens in console on UI, onAuthStateChanged shows the new token

level: "4"
Enter fullscreen mode Exit fullscreen mode

JS code to print user custom claims in console log:

    if(user) {
      user.getIdToken(true)
      .then(() => {
        user.getIdTokenResult().then(idTokenResult => {
          console.log('firebase-util.js idTokenResult.claims: ', idTokenResult.claims)
        })
      })
    } else {
      console.log('refreshIdTokens : currentUser not set')
    }
Enter fullscreen mode Exit fullscreen mode

In firebase documentation also, I see only Database Rules and Storage Rules are mentioned for Custom-Claims. I do not see reference of Firestore Rules - firebase.google.com/docs/auth/admi...

Collapse
 
pradeephere profile image
pradeep-here

Oh, got it sorted just now.

Issue: Custom claim (level) was String "4"
Fix: Changed it to Number 4

Collapse
 
alvardev profile image
Alvaro David

Hi Pradeep-here! Great to know that everything worked :)

Collapse
 
khromov profile image
Stanislav Khromov

Thank you Alvaro, this post was very useful in optimizing my application to use fewer get() calls!