Controlling Data Access Using Firebase Auth Custom Claims

Lots of apps require different levels of data access for different types of users. For example, you may have an app for teachers and students. Teachers need to be able to see all student data, add grades, and make exams, and students need to be able to see their own grades and take those exams.

A news app needs access for readers to view stories, editors to publish their own stories, and admins who can control all content. A movie review app will have lots of users leaving reviews, and then maybe a few moderators adding movie data and screening reviews. So this is a common pattern with lots of different solutions. I’m going to dive into one solution to this problem: Firebase Auth custom claims.

No matter how your user is signing in with Firebase Auth, whether it’s using Firebase’s email/password auth, a third party provider like Github or Google, or a custom auth token that you create yourself, at the end of the process you get back a Firebase ID token that identifies your user to the Firebase service.

When it comes to making graphics, clearly I’m an artist

Firebase Auth ID tokens are JWTs with some extra data in them about the Firebase user, like their email address and display name. In addition to this data, you can add custom claims to the token to assert things about the user. These assertions can be used within your app to grant role-based access. Custom claims are key/value pairs that you can set per user at the server level. They can be defined to give administrative privileges to access data and resources like you would need for apps with teachers and students, and provide multi-level access to data like you’d have in apps with paid and unpaid subscribers.

{
'email': 'fake_email@gmail.com',
'imageUrl': 'https://myphoto.jpg',
'claims': {
'moderator': true,
'paid': true
}
}

Use custom claims sparingly

It might sound like a good idea to add all sorts of criteria as custom claims. Perhaps you want to add a home address, additional photo URLs, or a profile description, for example. But this is not what custom claims are designed for. Data like addresses should be stored in a database, since it’s not related to authentication. Tokens are not profiles! And since the whole token is sent with every request to Firebase resources, a very large token can make your app slower and use more data. In general, you don’t add other properties to the Firebase ID token directly. Instead, you store the additional properties in Cloud Firestore or the Realtime Database and look them up as necessary. Note that custom claims are limited to a total size of 1000 bytes. If you find that you’re hitting that limit, you may need to rethink how you’re using claims, as it’s going to be tough to track a high number of claims.

Check out an example

With those precautions in mind, let’s get into how to apply those claims to grant limited access to your app’s data. For this example, I use a sample app I made for sharing movie ratings called FireFlicks. There’s a codelab that goes along with the FireFlicks app that you should really check out if you’re interested in custom claims.

In FireFlicks, I want a few choice moderators to have the ability to add new movies to the database. I want any user to be able to review movies, but I don’t want them to be able to add new movies to the database. I use custom claims to distinguish between regular users and moderators by adding a claim to the id token of moderators. I definitely don’t want to do that on the client. Instead, I send a request to my server to make those changes for me. Whether it’s a server you build yourself or a serverless solution like Cloud Functions, you’ll want to restrict who can interact with those services. Fortunately, you can manage and access claims from your server using the Firebase Admin SDK.

Generating custom claims using the Firebase Auth Admin SDK

In this example, I’m showing you how to get and set custom claims using the Admin SDK for Javascript. Specifically, I’m using TypeScript. You can also set claims using the Admin SDKs for Python, Go, and Java. See the guide for examples.

I use a callable Cloud Function to trigger my code. I’m not going to touch upon setting up Cloud Functions or initializing the Admin SDK, but I’ve linked to sources if you need them.

I’ve declared a function called grantModeratorRole, which takes one parameter, a user’s email. This is the email of the user to whom we want to grant moderator privileges.

async function grantModeratorRole(email) {
const user = await admin.auth().getUserByEmail(email); // 1
if (user.customClaims && user.customClaims.moderator === true) {
return;
} // 2
return admin.auth().setCustomUserClaims(user.uid, {
moderator: true
}); // 3
}

(1) I use a function from the Firebase Admin SDK, getUserByEmail, to get the Firebase user who has the given email.

(2) I check the user’s current claims using the .customClaims parameter. If the user already has the moderator claim, my work here is done, and I return.

(3) If the user does not, I then call the Admin SDK’s setCustomUserClaims function, which adds the claim to the user. You can add multiple claims at once, but remember that 1000 byte limit.

Keep in mind that if a user has logged in and then you add a custom claim, they won’t have it in their token until the next time the token is issued. The reverse is also true — if you take away access by removing a custom claim, access will not be immediately revoked on existing tokens. Each client SDK has a method to refresh the Firebase token, so you can use this where appropriate.

Checking for claims server-side

There’s an important piece missing from the code I just showed you: I need a way to ensure that only authorized users can add claims to other users. One way I can do this is by passing along the Firebase ID token of the user making the request. Then server-side, be it in a Cloud Function or whatever protected environment I’m using, I can check to see if the user is authorized to promote other users.

To check if the user making the request has permission, the code checks for claims on the user’s Firebase ID token. One option for doing this is to send this token along as a header in the request. If you’d like to see what this looks like, check out the codelab. The example code below achieves this using a callable Cloud Function, which automatically includes the Firebase Auth token as a header and expose it in the function context. This enables me to do something like this:

exports.addAdmin = functions.https.onCall((data, context) => {
if (context.auth.token.moderator !== true) { // 1
return {
error: "Request not authorized. User must be a moderator
to fulfill request."
};
}; // 2
const email = data.email; // 3
return grantModeratorRole(email).then(() => {
return {
result: `Request fulfilled! ${email} is now a
moderator.`
};
}); // 4
});

(1) Check for the moderator claim using context.auth.token.moderator.

(2) If the moderator claim isn’t set to true for that user, return an error. This error will get passed back to the client.

(3) If the user does have the moderator claim, get the email parameter from the data passed to the callable function.

(4) Pass the email from the request to the grantModeratorRole function I showed you earlier.

Applying custom claims in Firebase security rules

Perhaps the most powerful use of custom claims is with Firebase security rules. Since a user’s claims are accessible from security rules, you can restrict data access for Cloud Firestore, the Realtime Database, and Cloud Storage, based on the presence of claims.

Rules in Cloud Firestore and Cloud Storage share the same format. I’m going to assume you’re familiar with security rules already. If you’d like to find out more about security rules, check out the guides for Cloud Firestore, Cloud Storage, and the Realtime Database.

Claims in Cloud Firestore and Cloud Storage are accessed using request.auth.token.

Any claims present can be accessed using the key of the claim. So for Firestore and Storage, I can access a claim called “moderator” using request.auth.token.moderator. In FireFlicks, movie documents are stored in a collection called “movies”. I want to ensure that authenticated users can read movies, but only moderators can write to them.

service cloud.firestore {
match /databases/{database}/documents {
match /movies/{movie} {
allow read: if request.auth != null;
allow write: if request.auth.token.moderator == true;
}
}
}

Applying custom claims client-side

Custom claims provide a good way to implement role-based access to Firebase data.

But wait, there’s more! Custom claims are accessible client-side, too. This allows you to customize what users see based on their role. Here are some brief examples of what that looks like on iOS, Android, and Web.

Each of the client Auth SDKs has a function for getting the claims from the Firebase ID Token.

In Swift, I use the following code:

user.getIDTokenResult(forcingRefresh: true, completion: { (result, error) in // 1
guard let moderator = result?.claims?["moderator"]
as? NSNumber {
// 3 Show regular user UI.
showRegularUI()
return
}
if moderator.boolValue {
// 2 Show moderator UI.
showModeratorUI()
} else {
// 3 Show regular user UI.
showRegularUI()
}
})

(1) Call getIDTokenResult. Don’t confuse this with the getIDToken function which works similar, but passes along an encoded token. Also notice I force the token refresh to ensure that any newly added claims are on the token. If your app doesn’t make frequent changes to claims and doesn’t depend on claims to be up to date, you can pass just the closure as a parameter, which will set token refresh to the default of false.

(2) If the moderator claim is found, I show the UI with moderator privileges.

(3) If not, I show the regular UI.

On Android, I can determine the UI to show like this:

user.getIdToken(true).addOnSuccessListener(new OnSuccessListener<GetTokenResult>() { // 1
@Override
public void onSuccess(GetTokenResult result) {
boolean isModerator = result.getClaims().get("moderator"); // 2
if (isModerator) { // 3
// Show moderator UI
showModeratorUI();
} else {
// Show regular user UI.
showRegularUI();
}
}
});

(1) Call getIdToken to get the token, passing true to force the token to refresh if it’s important to know if claims have changed. If it’s not essential, you can pass false.

(2) Call getClaims() to get all of the claims, and get() to read the value of the moderator claim

(3) Update the UI based on whether or not the user is a moderator

On a web client using the Firebase JavaScript SDK, I can do the following:

firebase.auth().currentUser.getIdTokenResult(true) // 1
.then((idTokenResult) => {
// 2 Confirm the user is an Admin
if (idTokenResult.claims.moderator) { // 3
// Show moderator UI.
showModeratorUI();
} else {
// Show regular user UI.
showRegularUI();
}
})
.catch((error) => {
console.log(error);
});

(1) Call getIDTokenResult, passing true to force the refresh of the token if it’s essential to get the latest claims. You can pass no parameters to the function if it’s not essential to your app to have the latest claims. This function returns a promise which, when fulfilled, is an IdTokenResult object. This object contains the ID token JWT string, other helper properties for getting different data associated with the token, and all the decoded payload claims.

(2) Check for the moderator claim

(3) Display a different UI depending on the result.

Ensure data is securely protected

There’s a good chance that simply hiding or showing content based on the presence of claims will lead to a suitable experience for most of your users. But as a developer, you know this isn’t a method of securing data. For example, in a web app anyone can just open up the browser’s developer tools and inspect the code to find out what’s going on under the hood.

Using custom claims client-side should only be used to create a better user experience. It is not secure! To make sure that even a clever developer like you can’t access that moderator data, you need to integrate security on the backend as well through Firebase security rules. Think of using custom claims on the client as a means of guiding users in your app, not as a means of security.

Things to keep in mind when using custom claims

You’re almost ready to get cracking on custom claims! As you start looking for places to apply them in your app, here are some things to keep in mind:

  • Remember that custom claims need to be applied in a secure backend environment or users could add their own claims, which would defeat the purpose. You could use whatever server or serverless solution you prefer.
  • You’ll probably want to use some combination of claims on the frontend and backend to create the best user experience and ensure data security.
  • Setting a custom claim with the same key as an existing one will always cause an overwrite operation. This might be what you’re looking for, but can lead to undesirable consequences if it isn’t expected.
  • If your app uses custom tokens that you’ve generated, any claims set on the user will not be accessible. Claims set on custom tokens always have a higher priority, so it will be as though user claims don’t exist.

Alright, you’re now ready to use Firebase Auth custom claims to customize data access! I’d love to hear about how you’re using claims in your own apps! Let me know on Twitter at @ThatJenPerson.